Exportador de esquemas JSON

La clase JsonSchemaExporter, introducida en .NET 9, permite extraer documentos de esquema JSON de tipos .NET usando una instancia JsonSerializerOptions o JsonTypeInfo. El esquema resultante proporciona una especificación del contrato de serialización JSON para el tipo .NET. El esquema describe la forma de lo que sería serializado y lo que puede ser deserializado.

En el siguiente fragmento de código se muestra un ejemplo.

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);

Como se puede ver en este ejemplo, el exportador distingue entre propiedades que aceptan valores NULL y que no aceptan valores NULL y rellena la palabra clave required en función de si un parámetro de constructor es opcional o no.

A partir de .NET 11, el exportador reconoce los BFloat16tipos , Decimal32, Decimal64y Decimal128 . Se exportan esquemas para sus formas anulables y para literales con nombre al habilitar AllowNamedFloatingPointLiterals.

Esquemas para tipos de unión

A partir de .NET 11, JsonSchemaExporter describe una unión de C# con un esquema sin etiquetar anyOf que tiene una rama para cada caso. La unión no agrega ningún discriminador porque JsonSerializer escribe solo el caso activo. Por el contrario, al habilitar la inferencia para una jerarquía cerrada, JSON serializado como el tipo base cerrado incluye un $type discriminador que identifica el tipo derivado. El anyOf que se describe aquí corresponde a la salida de JsonSchemaExporter; ASP.NET Core genera documentos OpenAPI por separado.

Configuración de la salida del esquema

Puede influir en la salida del esquema con la configuración especificada en la instancia JsonSerializerOptions o JsonTypeInfo en la que se llama al método GetJsonSchemaAsNode. En el ejemplo siguiente se establece la directiva de nomenclatura en KebabCaseUpper, se escriben números como cadenas y se deshabilitan las propiedades no asignadas.

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; }
}

Puede controlar aún más el esquema generado mediante el tipo de configuración JsonSchemaExporterOptions. En el ejemplo siguiente se establece la propiedad TreatNullObliviousAsNonNullable en true para marcar los tipos de nivel raíz como que no aceptan valores NULL.

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);

Transformación del esquema generado

Puede aplicar sus propias transformaciones a los nodos de esquema generados especificando un delegado TransformSchemaNode. En el ejemplo siguiente se incorpora texto de anotaciones DescriptionAttribute en el esquema generado.

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;
    }
};

El ejemplo de código siguiente genera un esquema que incorpora el origen de palabra clave description de las anotaciones DescriptionAttribute:

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);