Home Source-Generated JSON Schemas
Source-Generated JSON Schemas
Cancel

Source-Generated JSON Schemas

Some systems disallow runtime reflection, and to run on these systems, your app needs to be built with Native AOT. Since the .FromType<T>() schema builder extension uses reflection, it can’t be uses on such system. To accommodate this, JsonSchema.Net.Generation provides source generation to create schemas at compile time.

When you decorate a type with [GenerateJsonSchema], a C# source generator runs during compilation and outputs static schema properties that you can use just like any other generated code. The system will also register these with the built-in JSON converters to support validation during serialization.

How it works

Decorate your types with [GenerateJsonSchema]:

1
2
3
4
5
6
7
8
9
10
11
12
[GenerateJsonSchema]
public class MyModel
{
    [Required]
    [MinLength(10)]
    [MaxLength(50)]
    public string Name { get; set; }
    
    [Minimum(0)]
    [Maximum(120)]
    public int Age { get; set; }
}

When you build your project, the source generator creates a static class called GeneratedJsonSchemas with a property for each decorated type:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static partial class GeneratedJsonSchemas
{
    public static readonly JsonSchema MyModel = 
        new JsonSchemaBuilder()
            .Type(SchemaValueType.Object)
            .Properties(
                ("Name", new JsonSchemaBuilder()
                    .Type(SchemaValueType.String)
                    .MinLength(10)
                    .MaxLength(50)),
                ("Age", new JsonSchemaBuilder()
                    .Type(SchemaValueType.Integer)
                    .Minimum(0)
                    .Maximum(120))
            )
            .Required("Name")
            .Build();
}

The primary purpose is validation during deserialization (covered in the next section), but you can also access the schemas directly if needed:

1
2
var schema = GeneratedJsonSchemas.MyModel;
var results = schema.Evaluate(myJsonData);

Configuration via the attribute

The attribute has a few options you can set:

1
2
3
4
5
6
7
8
9
10
[GenerateJsonSchema(
    PropertyNaming = NamingConvention.CamelCase,
    PropertyOrder = PropertyOrder.ByName,
    StrictConditionals = false
)]
public class MyModel
{
    public string FirstName { get; set; }  // becomes "firstName" in the schema
    public string LastName { get; set; }   // becomes "lastName" in the schema
}
  • PropertyNaming - How property names are transformed. Default is CamelCase.
  • PropertyOrder - Whether properties are listed as declared or sorted by name. Default is AsDeclared.
  • StrictConditionals - Advanced option for how conditionals are structured. See Conditionals for details.

The source generator uses the same attribute system as .FromType<T>(), so all of the attributes described in the basics page work here too.

Disabling source generation

If you need to use runtime generation instead, you can disable source generation by adding this to your project file:

1
2
3
<PropertyGroup>
    <DisableJsonSchemaSourceGeneration>true</DisableJsonSchemaSourceGeneration>
</PropertyGroup>

Remember that runtime generation won’t work with Native AOT.

Automatic validation integration

To use the generated schemas for validation during deserialization, add the GenerativeValidatingJsonConverter to your serializer options:

1
2
3
4
5
6
7
8
var options = new JsonSerializerOptions
{
    Converters = { new GenerativeValidatingJsonConverter() }
};

// Deserialization with automatic validation using the generated schema
var json = """{"name": "John Doe", "age": 30}""";
var model = JsonSerializer.Deserialize<MyModel>(json, options);

The source generator also creates code that automatically registers each generated schema.

Using the schemas directly

The generated schemas are just static properties, so you can use them however you like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Write to a file
var schemaJson = JsonSerializer.Serialize(
    GeneratedJsonSchemas.MyModel,
    new JsonSerializerOptions { WriteIndented = true }
);
File.WriteAllText("my-model-schema.json", schemaJson);

// Validate manually
var instance = JsonNode.Parse(someJson);
var results = GeneratedJsonSchemas.MyModel.Evaluate(instance);

// Combine with other schemas
var combined = new JsonSchemaBuilder()
    .AllOf(
        GeneratedJsonSchemas.MyModel,
        additionalConstraints
    ).Build();

Working with DataAnnotations

The source generator recognizes attributes from the JsonSchema.Net.Generation.DataAnnotations package alongside the built-in attributes. You can mix and match:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System.ComponentModel.DataAnnotations;
using Json.Schema.Generation;

[GenerateJsonSchema]
public class User
{
    [Required]  // from JsonSchema.Net.Generation
    [MaxLength(50)]  // from DataAnnotations
    [EmailAddress]  // from DataAnnotations - generates format: email
    public string Email { get; set; }
    
    [Range(18, 100)]  // from DataAnnotations
    [Description("User's age")]  // from JsonSchema.Net.Generation
    public int Age { get; set; }
}

The generator processes all of them and adds the appropriate keywords to the schema.

More on DataAnnotations support can be found on the Data Annotations page.

Custom attributes

You can create your own attributes that work with the source generator. It looks for implementations of IAttributeHandler, so if you’ve followed the instructions on the schema generation page, you’re halfway there.

The only thing you need to add to the handler, whether the attribute is its own handler or not, is an .Apply() method that appropriately extends a JsonSchemaBuilder passed to it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[AttributeUsage(AttributeTargets.Property)]
public class CreditCardAttribute : Attribute, IAttributeHandler<CreditCardAttribute>
{
    // For runtime generation with .FromType<T>()
    public void AddConstraints(SchemaGenerationContextBase context, Attribute attribute)
    {
        context.Intents.Add(new PatternIntent("^[0-9]{13,19}$"));
    }
    
    // For source generation
    public static JsonSchemaBuilder Apply(JsonSchemaBuilder builder)
    {
        return builder.Pattern("^[0-9]{13,19}$");
    }
}

The source generator will create a custom extension method that then calls this .Apply() method to augment the generated schema.

The JsonSchema.Net.Generation.DataAnnotations package has plenty of examples.

Writing schemas by hand

If the generated schema just isn’t doing what you want, it may be better (or necessary) to just write the schema yourself:

1
2
3
4
5
6
7
8
var schema = new JsonSchemaBuilder()
    .Type(SchemaValueType.Object)
    .Properties(
        ("name", new JsonSchemaBuilder().Type(SchemaValueType.String).MinLength(1)),
        ("age", new JsonSchemaBuilder().Type(SchemaValueType.Integer).Minimum(0))
    )
    .Required("name", "age")
    .Build();

This gives you complete control over the schema, but you’ll need to keep it in sync with your types manually.

Contents