Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
The JsonSchemaExporter class, introduced in .NET 9, lets you extract JSON schema documents from .NET types using either a JsonSerializerOptions or JsonTypeInfo instance. The resultant schema provides a specification of the JSON serialization contract for the .NET type. The schema describes the shape of what would be serialized and what can be deserialized.
The following code snippet shows an example.
public static void SimpleExtraction()
{
JsonSerializerOptions options = JsonSerializerOptions.Default;
JsonNode schema = options.GetJsonSchemaAsNode(typeof(Person));
Console.WriteLine(schema.ToString());
//{
// "type": ["object", "null"],
// "properties": {
// "Name": { "type": "string" },
// "Age": { "type": "integer" },
// "Address": { "type": ["string", "null"], "default": null }
// },
// "required": ["Name", "Age"]
//}
}
record Person(string Name, int Age, string? Address = null);
As can be seen in this example, the exporter distinguishes between nullable and non-nullable properties, and it populates the required keyword by virtue of a constructor parameter being optional or not.
Starting in .NET 11, the exporter recognizes the BFloat16, Decimal32, Decimal64, and Decimal128 types. It exports schemas for their nullable forms and for named literals when you enable AllowNamedFloatingPointLiterals.
Schemas for union types
Starting in .NET 11, JsonSchemaExporter describes a C# union with an untagged anyOf schema that has a branch for each case. The union adds no discriminator because JsonSerializer writes only the active case. By contrast, when you enable inference for a closed hierarchy, JSON serialized as the closed base type includes a $type discriminator that identifies the derived type. The anyOf described here is JsonSchemaExporter output; ASP.NET Core generates OpenAPI documents separately.
Configure the schema output
You can influence the schema output by configuration specified in the JsonSerializerOptions or JsonTypeInfo instance that you call the GetJsonSchemaAsNode method on. The following example sets the naming policy to KebabCaseUpper, writes numbers as strings, and disallows unmapped properties.
public static void CustomExtraction()
{
JsonSerializerOptions options = new(JsonSerializerOptions.Default)
{
PropertyNamingPolicy = JsonNamingPolicy.KebabCaseUpper,
NumberHandling = JsonNumberHandling.WriteAsString,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
};
JsonNode schema = options.GetJsonSchemaAsNode(typeof(MyPoco));
Console.WriteLine(schema.ToString());
//{
// "type": ["object", "null"],
// "properties": {
// "NUMERIC-VALUE": {
// "type": ["string", "integer"],
// "pattern": "^-?(?:0|[1-9]\\d*)$"
// }
// },
// "additionalProperties": false
//}
}
class MyPoco
{
public int NumericValue { get; init; }
}
You can further control the generated schema using the JsonSchemaExporterOptions configuration type. The following example sets the TreatNullObliviousAsNonNullable property to true to mark root-level types as non-nullable.
public static void CustomExtraction()
{
JsonSerializerOptions options = JsonSerializerOptions.Default;
JsonSchemaExporterOptions exporterOptions = new()
{
TreatNullObliviousAsNonNullable = true,
};
JsonNode schema = options.GetJsonSchemaAsNode(typeof(Person), exporterOptions);
Console.WriteLine(schema.ToString());
//{
// "type": "object",
// "properties": {
// "Name": { "type": "string" }
// },
// "required": ["Name"]
//}
}
record Person(string Name);
Transform the generated schema
You can apply your own transformations to generated schema nodes by specifying a TransformSchemaNode delegate. The following example incorporates text from DescriptionAttribute annotations into the generated schema.
JsonSchemaExporterOptions exporterOptions = new()
{
TransformSchemaNode = (context, schema) =>
{
// Determine if a type or property and extract the relevant attribute provider.
ICustomAttributeProvider? attributeProvider = context.PropertyInfo is not null
? context.PropertyInfo.AttributeProvider
: context.TypeInfo.Type;
// Look up any description attributes.
DescriptionAttribute? descriptionAttr = attributeProvider?
.GetCustomAttributes(inherit: true)
.Select(attr => attr as DescriptionAttribute)
.FirstOrDefault(attr => attr is not null);
// Apply description attribute to the generated schema.
if (descriptionAttr != null)
{
if (schema is not JsonObject jObj)
{
// Handle the case where the schema is a Boolean.
JsonValueKind valueKind = schema.GetValueKind();
Debug.Assert(valueKind is JsonValueKind.True or JsonValueKind.False);
schema = jObj = new JsonObject();
if (valueKind is JsonValueKind.False)
{
jObj.Add("not", true);
}
}
jObj.Insert(0, "description", descriptionAttr.Description);
}
return schema;
}
};
The following code example generates a schema that incorporates description keyword source from DescriptionAttribute annotations:
JsonSerializerOptions options = JsonSerializerOptions.Default;
JsonNode schema = options.GetJsonSchemaAsNode(typeof(Person), exporterOptions);
Console.WriteLine(schema.ToString());
//{
// "description": "A person",
// "type": ["object", "null"],
// "properties": {
// "Name": { "description": "The name of the person", "type": "string" }
// },
// "required": ["Name"]
//}
[Description("A person")]
record Person([property: Description("The name of the person")] string Name);