Description
Description
JsonSchemaExporer.GetJsonSchemaAsNode
has an extra heuristic that will mark constructor parameters of classes as required
in the schema it creates.
This means that it does not match the runtime behavior. Properties are marked as required that are not required at runtime.
However, this heuristic does not apply to structs. Switching the same definition between record class
and record struct
results in different schemas but the same deserialization behavior. (See repro).
This is similar to the discussion here: dotnet/extensions#6080
The JsonTypeInfo
is different for record struct
and record
because the record struct
will have a parameterless constructor. As a result there are behavior differences when constructor parameter definitions/attributes result in different schemas for classes and structs.
--
This heuristic is unhelpful for my scenario 😢 and in an ideal world I could turn it off entirely.
I'm using the schemas to understand what the serializer will accept, and to diff changes to our code over time for breaking change detection. Any place where the schema generation is adding additional heuristics or assumptions that don't match the runtime behavior is another thing I have to work around.
I discovered this as part of a change where someone flipped a class to a struct, and the properties became non-required in the schema.
Reproduction Steps
using System.Text.Json;
using System.Text.Json.Schema;
using System.Text.Json.Serialization.Metadata;
var opts = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
};
void Test<T>(string json)
{
JsonTypeInfo info = JsonSerializerOptions.Default.GetTypeInfo(typeof(T));
foreach (var property in info.Properties)
{
if (property.IsRequired)
{
Console.WriteLine($"Property {property.Name} in {typeof(T).Name} is required.");
}
else
{
Console.WriteLine($"Property {property.Name} in {typeof(T).Name} is optional.");
}
}
var schema = info.GetJsonSchemaAsNode();
Console.WriteLine($"JSON Schema for {typeof(T).Name}:\n{schema.ToJsonString(opts)}");
try
{
var obj = JsonSerializer.Deserialize<T>(json);
Console.WriteLine($"Deserialized {typeof(T).Name} successfully: {obj}");
}
catch (JsonException ex)
{
Console.WriteLine($"Failed to deserialize {typeof(T).Name}: {ex.Message}");
}
}
var json = "{}";
Test<AClass>(json);
Test<AStruct>(json);
public sealed record AClass(string Name, DateTimeOffset Birthday);
public readonly record struct AStruct(string Name, DateTimeOffset Birthday);
Expected behavior
I'd really like the schema output to match the runtime behavior. In this example that would be AClass
having no required
properties.
Actual behavior
Property Name in AClass is optional.
Property Birthday in AClass is optional.
JSON Schema for AClass:
{
"type": [
"object",
"null"
],
"properties": {
"Name": {
"type": "string"
},
"Birthday": {
"type": "string",
"format": "date-time"
}
},
"required": [
"Name",
"Birthday"
]
}
Deserialized AClass successfully: AClass { Name = , Birthday = 1/1/0001 12:00:00 AM +00:00 }
Property Name in AStruct is optional.
Property Birthday in AStruct is optional.
JSON Schema for AStruct:
{
"type": "object",
"properties": {
"Name": {
"type": "string"
},
"Birthday": {
"type": "string",
"format": "date-time"
}
}
}
Deserialized AStruct successfully: AStruct { Name = , Birthday = 1/1/0001 12:00:00 AM +00:00 }
Regression?
No response
Known Workarounds
I could TransformSchemaNode
and re-implement my own version of required
.
Configuration
.NET SDK:
Version: 9.0.300
Commit: 15606fe0a8
Workload version: 9.0.300-manifests.c678e91b
MSBuild version: 17.14.5+edd3bbf37
Other information
No response