Home Automatic API Request Validation
Automatic API Request Validation
Cancel

Automatic API Request Validation

JsonSchema.Net.Api provides automatic JSON Schema validation for ASP.NET Core MVC request bodies, integrating seamlessly with the MVC pipeline to validate incoming data before it reaches your controllers.

This library combines the power of JsonSchema.Net and JsonSchema.Net.Generation to deliver built-in request validation with detailed error reporting.

More information about schema-based validation can be found in the Enhancing Deserialization with JSON Schema documentation.

The same schemas can describe your API in OpenAPI 3.1. See Describing APIs with OpenAPI.

By default, this library uses source generation to create schemas at compile time, which works with Native AOT. If you need runtime schema generation, you can enable it via configuration, but that won’t work with Native AOT.

Configuration

To enable automatic validation, add the JSON Schema validation services to your MVC configuration in Program.cs or Startup.cs:

1
2
3
4
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .AddJsonSchemaValidation();

This single call configures:

  • A validation filter that intercepts requests and validates JSON bodies
  • A custom model binder that performs schema validation during model binding
  • JSON serialization options with the generative validating converter
  • An enum converter matching what the generated schemas describe (see Enumerations)

Customizing validation behavior

You can customize the validation behavior by passing a configuration delegate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
builder.Services.AddControllers()
    .AddJsonSchemaValidation(converter =>
    {
        // Specify dialect or register other dialects, vocabularies, or external schemas
        converter.BuildOptions.Dialect = Dialect.Draft202012;

        // Customize schema generation (for runtime generation only)
        converter.GeneratorConfiguration.PropertyNameResolver = PropertyNameResolvers.SnakeCase;
        converter.GeneratorConfiguration.Nullability = Nullability.AllowForNullableValueTypes;
        
        // Customize schema evaluation
        converter.EvaluationOptions.OutputFormat = OutputFormat.List;
        converter.EvaluationOptions.RequireFormatValidation = true;
    });

If no configuration is provided, the following defaults are used:

  • Property names are converted to camelCase
  • Output format is hierarchical (required to report errors properly)
  • format validation is required

JSON Schema typically does not validate the format keyword, however users generally expect this behavior, so it has been enabled by default for request body validation.

When using source generation (the default), property naming, property order, and strict conditionals must be configured on the [GenerateJsonSchema] attribute itself, not in GeneratorConfiguration. Runtime-only settings like PropertyNameResolver and Nullability in GeneratorConfiguration only apply when you disable source generation.

Enumerations

Source-generated schemas describe an enum by its member names by default, but System.Text.Json’s default configuration only accepts integers. Left alone, a request body with "category": "Toys" would pass schema validation and then fail deserialization.

To keep the two in agreement, validation registers the converter that matches the enum format the schemas describe:

JsonSchemaDefaultEnumFormatConverter registered
Names (default)JsonStringEnumConverter(allowIntegerValues: false)
NamesAndValuesJsonStringEnumConverter()
Valuesnone; integers are the default

The converter is appended to both the MVC and the minimal-API JsonSerializerOptions, so a JsonStringEnumConverter the application registered first, or a [JsonConverter] on the enum type itself, wins. Nothing is added when a JsonStringEnumConverter is already present.

When the application spans several assemblies with [GenerateJsonSchema] types, one serializer configuration has to satisfy all of them: if any assembly describes names and any describes integers, the converter accepts both.

See Enumerations in the source generation docs for how the schema side is decided, including per-type and per-property [JsonConverter] handling.

This registration was added in JsonSchema.Net.Api v1.2.1 with JsonSchema.Net.Generation v7.4.0.

Defining validation schemas

To validate a model, decorate it with the [GenerateJsonSchema] attribute and any applicable validation attributes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[GenerateJsonSchema]  // Source generation is used by default
[AdditionalProperties(false)]
public class CreateUserRequest
{
    [Required]
    [MinLength(3)]
    [MaxLength(50)]
    public string Username { get; set; }
    
    [Required]
    [Pattern(@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")]
    public string Email { get; set; }
    
    [Required]
    [Minimum(18)]
    [Maximum(120)]
    public int Age { get; set; }
}

If you need to customize property naming or ordering for source generation, configure it on the attribute:

1
2
3
4
5
6
7
8
[GenerateJsonSchema(
    PropertyNaming = NamingConvention.SnakeCase,
    PropertyOrder = PropertyOrder.ByName
)]
public class CreateUserRequest
{
    // properties...
}

Alternatively, if the generated schema doesn’t meet your requirements, you can use the [JsonSchema()] attribute to specify your own schema explicitly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[JsonSchema(typeof(UserSchemas), nameof(UserSchemas.CreateUserRequestSchema))]
public class CreateUserRequest
{
    public string Username { get; set; }
    public string Email { get; set; }
    public int Age { get; set; }
}

public static class UserSchemas
{
    public static readonly JsonSchema CreateUserRequestSchema =
        new JsonSchemaBuilder()
            .Type(SchemaValueType.Object)
            .Properties(
                ("username", new JsonSchemaBuilder()
                    .Type(SchemaValueType.String)
                    .MinLength(3)
                    .MaxLength(50)
                ),
                ("email", new JsonSchemaBuilder()
                    .Type(SchemaValueType.String)
                    .Pattern(@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
                ),
                ("age", new JsonSchemaBuilder()
                    .Type(SchemaValueType.Integer)
                    .Minimum(18)
                    .Maximum(120)
                )
            )
            .Required("username", "email", "age")
            .AdditionalProperties(false);
}

When a request with a JSON body is received, the library will:

  1. Generate a JSON Schema from the model type (or use a cached version)
  2. Validate the incoming JSON against the schema
  3. Deserialize the JSON if validation passes
  4. Return a detailed error response if validation fails

Schema validation will only occur for models decorated with [GenerateJsonSchema] or [JsonSchema()]. Models without these attributes will be deserialized normally without validation.

When using [GenerateJsonSchema], the attribute is only required on the top-level model (the controller action parameter). Child models referenced by properties will automatically be included in the generated schema without needing the [GenerateJsonSchema] attribute themselves, though they should still have validation attributes applied to their properties.

Using validated models in controllers

Once configured, simply use your models as controller action parameters:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    [HttpPost]
    public IActionResult CreateUser([FromBody] CreateUserRequest request)
    {
        // If we reach here, the request has been validated
        // No additional validation code needed!
        
        var user = _userService.CreateUser(request);
        return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
    }
}

Error responses

When validation fails, the library automatically returns a standardized error response with HTTP status 400 (Bad Request) in the RFC 7807 Problem Details format:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
  "type": "https://json-everything.net/errors/validation",
  "title": "Validation Error",
  "status": 400,
  "detail": "One or more validation errors occurred.",
  "errors": {
    "/username": [
      "The value must have a minimum length of 3"
    ],
    "/email": [
      "The value does not match the required pattern"
    ]
  }
}

The error paths use JSON Pointer notation, making it easy to map errors to specific fields in your request.

How it works

The library integrates with ASP.NET Core through three main components:

ValidatingJsonModelBinder

This custom model binder intercepts the model binding process to:

  • Read the request body
  • Deserialize using the configured JSON serializer (with validation)
  • Capture validation errors from the ValidatingJsonConverter
  • Add errors to ModelState using JSON Pointer paths

JsonSchemaValidationFilter

This filter implements both IActionFilter and IAlwaysRunResultFilter to:

  • Inspect ModelState for validation errors after model binding
  • Filter errors to only those with JSON Pointer paths (ensuring they came from schema validation)
  • Build a Problem Details response with structured error information
  • Short-circuit the request pipeline when validation errors are present

GenerativeValidatingJsonConverter

This converter, provided by JsonSchema.Net.Generation, handles:

  • Generating schemas from types decorated with [GenerateJsonSchema]
  • Validating JSON during deserialization
  • Caching generated schemas for performance
  • Throwing JsonException with validation results when validation fails

Best practices

  1. Use validation attributes liberally - The more constraints you define, the safer your code and the more meaningful validation errors you’ll provide to API consumers.
  2. Test your schemas - Use unit tests to verify that your validation attributes produce the expected schemas and validation behavior.
  3. Consider separate validation models - For complex scenarios, create dedicated request models separate from your domain entities to keep validation concerns isolated.
  4. Customize error messages - The default validation errors are descriptive, but you can provide custom error messages to better match your application’s needs.

Example: Complete setup

Here’s a complete example showing all the pieces together:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .AddJsonSchemaValidation(converter =>
    {
        // Configure schema evaluation options
        converter.EvaluationOptions.RequireFormatValidation = true;
    });

var app = builder.Build();
app.MapControllers();
app.Run();

// Models/CreateProductRequest.cs
[GenerateJsonSchema(PropertyNaming = NamingConvention.CamelCase)]
[AdditionalProperties(false)]
public class CreateProductRequest
{
    [Required]
    [MinLength(1)]
    [MaxLength(100)]
    public string Name { get; set; }
    
    [MinLength(10)]
    [MaxLength(500)]
    public string? Description { get; set; }
    
    [Minimum(0.01)]
    public decimal Price { get; set; }
    
    [MinItems(1)]
    [UniqueItems(true)]
    public List<string> Tags { get; set; }
}

// Controllers/ProductsController.cs
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpPost]
    public IActionResult CreateProduct([FromBody] CreateProductRequest request)
    {
        // Fully validated request is available here
        return Ok(new { message = "Product created successfully" });
    }
}

With this setup, any request that doesn’t match the schema will be automatically rejected before reaching your controller code, keeping your business logic clean and focused.

Contents