Skip to content

[API Proposal]: Add a JSON schema exporting component for STJ contracts #102788

@eiriktsarpalis

Description

@eiriktsarpalis

Introduction

This issue defines the JSON schema exporting APIs largely following the design of the stj-schema-mapper prototype. Rather than introducing a JSON schema exchange type, the proposed exporter methods generate schema documents represented as JsonNode instances which can be modified or mapped to other schema models as required.

The exporter employs a callback model allowing users to enrich the generated schema for every node in the generated type graph using metadata from arbitrary attribute annotations.

Contributes to #100159

API Proposal

namespace System.Text.Json.Schema; // New namespace

public static class JsonSchemaExporter
{
    public static JsonNode GetJsonSchemaAsNode(this JsonSerializerOptions options, Type type, JsonSchemaExporterOptions? exporterOptions = null);
    public static JsonNode GetJsonSchemaAsNode(this JsonTypeInfo typeInfo, JsonSchemaExporterOptions? exporterOptions = null);
}

public sealed class JsonSchemaExporterOptions
{
    /// The default options singleton.
    public static JsonSchemaExporterOptions Default { get; } = new();

    /// Determines whether schema references should be generated for recurring schema nodes.
    public bool AllowSchemaReferences { get; init; } = true;

    /// Determines the compatibility mode used by the exporter.
    public JsonSchemaExporterCompatibilityMode CompatibilityMode { get; init; } = JsonSchemaExporterCompatibilityMode.Strict;

    /// Defines a callback that is invoked for every schema that is generated within the type graph.
    public Action<JsonSchemaExporterContext, JsonObject>? OnSchemaNodeGenerated { get; init; }
}

/// Context of the current schema node being generated.
public readonly struct JsonSchemaExporterContext
{
    /// The parent collection type if the schema is being generated for a collection element or dictionary value.
    public ReadOnlySpan<string> Path { get; }

    /// The JsonTypeInfo for the type being processed.
    public JsonTypeInfo TypeInfo { get; }

    /// The JsonPropertyInfo if the schema is being generated for a property.
    public JsonPropertyInfo? PropertyInfo { get; }
}

public enum JsonSchemaExporterCompatibilityMode
{
    /// Generates schemas that are strict with respect to nullability annotations and constructor parameter requiredness.
    Strict = 0,

    /// Generates schemas that are more permissive but preserve compatibility with the specified JsonSerializerOptions.
    JsonSerializer = 1,
}

API Usage

Here's an example using the callback API to extract schema information from other attributes:

var options = new JsonSchemaExporterOptions
{
    OnSchemaNodeGenerated = static (ctx, schema) =>
    {
        DescriptionAttribute? descriptionAttribute = ctx.PropertyInfo?.AttributeProvider?
             .GetCustomAttribute<DescriptionAttribute>();

        if (descriptionAttribute != null)
        {
            schema["description"] = (JsonNode)descriptionAttribute.Description;
        }
    }
});

JsonNode node = JsonSerializerOptions.Default.GetJsonSchemaAsNode(typeof(MyPoco), options);
// { "type" : "object", "properties" : { "X" : { "type" : "integer", "description" : "This is property X" } } }

public class MyPoco
{
    [Description("This is property X")]
    public int X { get; set; }
}

cc @captainsafia @stephentoub @gregsdennis

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions