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.
This library uses reflection to generate schemas and may not be compatible with Native AOT applications.
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
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
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)
formatvalidation is required
JSON Schema typically does not validate the
formatkeyword, however users generally expect this behavior, so it has been enabled by default for request body validation.
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]
[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; }
}
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:
- Generate a JSON Schema from the model type (or use a cached version)
- Validate the incoming JSON against the schema
- Deserialize the JSON if validation passes
- 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
ModelStateusing JSON Pointer paths
JsonSchemaValidationFilter
This filter implements both IActionFilter and IAlwaysRunResultFilter to:
- Inspect
ModelStatefor 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
JsonExceptionwith validation results when validation fails
Best practices
- 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.
- Test your schemas - Use unit tests to verify that your validation attributes produce the expected schemas and validation behavior.
- Consider separate validation models - For complex scenarios, create dedicated request models separate from your domain entities to keep validation concerns isolated.
- 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 =>
{
converter.GeneratorConfiguration.PropertyNameResolver = PropertyNameResolvers.CamelCase;
converter.EvaluationOptions.RequireFormatValidation = true;
});
var app = builder.Build();
app.MapControllers();
app.Run();
// Models/CreateProductRequest.cs
[GenerateJsonSchema]
[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.