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
Description
JsonSchemaExporer.GetJsonSchemaAsNodehas an extra heuristic that will mark constructor parameters of classes asrequiredin 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 classandrecord structresults in different schemas but the same deserialization behavior. (See repro).This is similar to the discussion here: dotnet/extensions#6080
The
JsonTypeInfois different forrecord structandrecordbecause therecord structwill 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
Expected behavior
I'd really like the schema output to match the runtime behavior. In this example that would be
AClasshaving norequiredproperties.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
TransformSchemaNodeand re-implement my own version ofrequired.Configuration
Other information
No response