Skip to content

JsonSchemaExporter.GetJsonSchemaAsNode is inconsistent with required properties + constructors + record structs #116172

Open
@rynowak

Description

@rynowak

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions