En la versión preliminar 6 de .NET 9 se incorporó JsonSchemaExporter, una herramienta diseñada para generar esquemas JSON compatibles con la especificación JSON Schema (Draft 2020-12) directamente a partir de tipos CLR. Esta funcionalidad simplifica notablemente la creación de contratos estructurados para APIs, validación de payloads o integración con herramientas externas como generadores de clientes OpenAPI.
Ejemplo básico
Considere el siguiente modelo:
public record Position
{
public int Code { get; init; }
public string Name { get; init; } = string.Empty;
public string? Notes { get; init; }
}
Para obtener su esquema JSON usando las opciones predeterminadas:
var options = new JsonSerializerOptions();
var schemaNode = JsonSchemaExporter.GetJsonSchemaAsNode(options, typeof(Position));
Console.WriteLine(JsonSerializer.Serialize(schemaNode, new JsonSerializerOptions { WriteIndented = true }));
La salida resultante será:
{
"type": ["object", "null"],
"properties": {
"Code": { "type": "integer" },
"Name": { "type": "string" },
"Notes": { "type": ["string", "null"] }
}
}
Influencia de JsonSerializerOptions
El comportamiento del exportador depende fuertemente de las opciones de serialización. Por ejemplo, al usar JsonSerializerOptions.Web, que aplica camelCase y admite conversión flexible entre cadenas y números:
var webOptions = JsonSerializerOptions.Web;
var webSchema = webOptions.GetJsonSchemaAsNode(typeof(Position));
Console.WriteLine(JsonSerializer.Serialize(webSchema, webOptions));
Produce:
{
"type": ["object", "null"],
"properties": {
"code": {
"type": ["string", "integer"],
"pattern": "^-?(?:0|[1-9]\\d*)$"
},
"name": { "type": "string" },
"notes": { "type": ["string", "null"] }
}
}
Obsérvese cómo code ahora acepta tanto enteros como cadenas numéricas, con una expresión regular que restringe los valores válidos —una característica inherente al perfil Web.
Campos obligatorios
Para marcar propiedades como requeridas, basta usar el modificador required (disponible desde C# 11):
public record Position
{
public int Code { get; init; }
public required string Name { get; init; }
public string? Notes { get; init; }
}
Esto genera automáticamente una sección "required" en el esquema:
"required": ["name"]
Personalización avanzada
Mediante JsonSchemaExporterOptions, es posible modificar el nodo generado antes de su serilaización. El siguiente ejemplo asegura que cualqueir propiedad cuyo nombre coinciad con "code" o "Code" se incluya en required, incluso si no está marcada explícitamente:
var customOptions = new JsonSchemaExporterOptions
{
TransformSchemaNode = (context, node) =>
{
var clone = node.DeepClone();
var targetProps = new[] { "code", "Code" };
if (clone["properties"] is JsonObject props)
{
var requiredArray = clone["required"] as JsonArray ?? new JsonArray();
foreach (var prop in targetProps)
{
if (props.ContainsKey(prop) && !requiredArray.Any(x => x.GetValueKind() == JsonValueKind.String && x.GetString() == prop))
{
requiredArray.Add(prop);
}
}
clone["required"] = requiredArray;
}
return clone;
}
};
var enhancedSchema = JsonSerializerOptions.Web.GetJsonSchemaAsNode(typeof(Position), customOptions);
El resultado incluye ambos campos requeridos:
"required": ["name", "code"]
Consideraciones finales
Aunque JsonSchemaExporter representa un avance significativo, su soporte actual es limitado: no cubre anotaciones personalizadas, referencias recursivas, ni validaciones complejas como allOf, oneOf o patrones condicionales. Para escenarios avanzados, sigue siendo necesario extenderlo mediante TransformSchemaNode o complementarlo con bibliotecas especializadas. Se espera que su funcionalidad se amplíe en futuras versiones del runtime.