|
| 1 | +--- |
| 2 | +layout: page |
| 3 | +title: Automatic API Request Validation |
| 4 | +bookmark: API Validation |
| 5 | +permalink: /schema/:title/ |
| 6 | +icon: fas fa-tag |
| 7 | +order: "01.025" |
| 8 | +--- |
| 9 | +_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. |
| 10 | + |
| 11 | +This library combines the power of [_JsonSchema.Net_](https://www.nuget.org/packages/JsonSchema.Net) and [_JsonSchema.Net.Generation_](https://www.nuget.org/packages/JsonSchema.Net.Generation) to deliver built-in request validation with detailed error reporting. |
| 12 | + |
| 13 | +> More information about schema-based validation can be found in the [Enhancing Deserialization with JSON Schema](/schema/serialization/) documentation. |
| 14 | +{: .prompt-tip } |
| 15 | + |
| 16 | +> This library uses reflection to generate schemas and may not be compatible with Native AOT applications. |
| 17 | +{: .prompt-warning } |
| 18 | + |
| 19 | +## Configuration {#schema-api-configuration} |
| 20 | + |
| 21 | +To enable automatic validation, add the JSON Schema validation services to your MVC configuration in `Program.cs` or `Startup.cs`: |
| 22 | + |
| 23 | +```c# |
| 24 | +var builder = WebApplication.CreateBuilder(args); |
| 25 | + |
| 26 | +builder.Services.AddControllers() |
| 27 | + .AddJsonSchemaValidation(); |
| 28 | +``` |
| 29 | + |
| 30 | +This single call configures: |
| 31 | + |
| 32 | +- A validation filter that intercepts requests and validates JSON bodies |
| 33 | +- A custom model binder that performs schema validation during model binding |
| 34 | +- JSON serialization options with the generative validating converter |
| 35 | + |
| 36 | +### Customizing validation behavior {#schema-api-custom-config} |
| 37 | + |
| 38 | +You can customize the validation behavior by passing a configuration delegate: |
| 39 | + |
| 40 | +```c# |
| 41 | +builder.Services.AddControllers() |
| 42 | + .AddJsonSchemaValidation(converter => |
| 43 | + { |
| 44 | + // Specify dialect or register other dialects, vocabularies, or external schemas |
| 45 | + converter.BuildOptions.Dialect = Dialect.Draft202012; |
| 46 | + |
| 47 | + // Customize schema generation |
| 48 | + converter.GeneratorConfiguration.PropertyNameResolver = PropertyNameResolvers.SnakeCase; |
| 49 | + converter.GeneratorConfiguration.Nullability = Nullability.AllowForNullableValueTypes; |
| 50 | + |
| 51 | + // Customize schema evaluation |
| 52 | + converter.EvaluationOptions.OutputFormat = OutputFormat.List; |
| 53 | + converter.EvaluationOptions.RequireFormatValidation = true; |
| 54 | + }); |
| 55 | +``` |
| 56 | + |
| 57 | +If no configuration is provided, the following defaults are used: |
| 58 | + |
| 59 | +- Property names are converted to `camelCase` |
| 60 | +- Output format is hierarchical (required to report errors properly) |
| 61 | +- `format` validation is required |
| 62 | + |
| 63 | +> 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. |
| 64 | +{: .prompt-info} |
| 65 | + |
| 66 | +## Defining validation schemas {#schema-api-schemas} |
| 67 | + |
| 68 | +To validate a model, decorate it with the `[GenerateJsonSchema]` attribute and any applicable validation attributes: |
| 69 | + |
| 70 | +```c# |
| 71 | +[GenerateJsonSchema] |
| 72 | +[AdditionalProperties(false)] |
| 73 | +public class CreateUserRequest |
| 74 | +{ |
| 75 | + [Required] |
| 76 | + [MinLength(3)] |
| 77 | + [MaxLength(50)] |
| 78 | + public string Username { get; set; } |
| 79 | + |
| 80 | + [Required] |
| 81 | + [Pattern(@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")] |
| 82 | + public string Email { get; set; } |
| 83 | + |
| 84 | + [Required] |
| 85 | + [Minimum(18)] |
| 86 | + [Maximum(120)] |
| 87 | + public int Age { get; set; } |
| 88 | +} |
| 89 | +``` |
| 90 | + |
| 91 | +Alternatively, if the generated schema doesn't meet your requirements, you can use the `[JsonSchema()]` attribute to specify your own schema explicitly: |
| 92 | + |
| 93 | +```c# |
| 94 | +[JsonSchema(typeof(UserSchemas), nameof(UserSchemas.CreateUserRequestSchema))] |
| 95 | +public class CreateUserRequest |
| 96 | +{ |
| 97 | + public string Username { get; set; } |
| 98 | + public string Email { get; set; } |
| 99 | + public int Age { get; set; } |
| 100 | +} |
| 101 | + |
| 102 | +public static class UserSchemas |
| 103 | +{ |
| 104 | + public static readonly JsonSchema CreateUserRequestSchema = |
| 105 | + new JsonSchemaBuilder() |
| 106 | + .Type(SchemaValueType.Object) |
| 107 | + .Properties( |
| 108 | + ("username", new JsonSchemaBuilder() |
| 109 | + .Type(SchemaValueType.String) |
| 110 | + .MinLength(3) |
| 111 | + .MaxLength(50) |
| 112 | + ), |
| 113 | + ("email", new JsonSchemaBuilder() |
| 114 | + .Type(SchemaValueType.String) |
| 115 | + .Pattern(@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$") |
| 116 | + ), |
| 117 | + ("age", new JsonSchemaBuilder() |
| 118 | + .Type(SchemaValueType.Integer) |
| 119 | + .Minimum(18) |
| 120 | + .Maximum(120) |
| 121 | + ) |
| 122 | + ) |
| 123 | + .Required("username", "email", "age") |
| 124 | + .AdditionalProperties(false); |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | +When a request with a JSON body is received, the library will: |
| 129 | + |
| 130 | +1. Generate a JSON Schema from the model type (or use a cached version) |
| 131 | +2. Validate the incoming JSON against the schema |
| 132 | +3. Deserialize the JSON if validation passes |
| 133 | +4. Return a detailed error response if validation fails |
| 134 | + |
| 135 | +> Schema validation will only occur for models decorated with `[GenerateJsonSchema]` or `[JsonSchema()]`. Models without these attributes will be deserialized normally without validation. |
| 136 | +{: .prompt-info } |
| 137 | + |
| 138 | +> 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. |
| 139 | +{: .prompt-tip } |
| 140 | + |
| 141 | +## Using validated models in controllers {#schema-api-usage} |
| 142 | + |
| 143 | +Once configured, simply use your models as controller action parameters: |
| 144 | + |
| 145 | +```c# |
| 146 | +[ApiController] |
| 147 | +[Route("api/[controller]")] |
| 148 | +public class UsersController : ControllerBase |
| 149 | +{ |
| 150 | + [HttpPost] |
| 151 | + public IActionResult CreateUser([FromBody] CreateUserRequest request) |
| 152 | + { |
| 153 | + // If we reach here, the request has been validated |
| 154 | + // No additional validation code needed! |
| 155 | + |
| 156 | + var user = _userService.CreateUser(request); |
| 157 | + return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user); |
| 158 | + } |
| 159 | +} |
| 160 | +``` |
| 161 | + |
| 162 | +## Error responses {#schema-api-errors} |
| 163 | + |
| 164 | +When validation fails, the library automatically returns a standardized error response with HTTP status 400 (Bad Request) in the [RFC 7807 Problem Details](https://tools.ietf.org/html/rfc7807) format: |
| 165 | + |
| 166 | +```json |
| 167 | +{ |
| 168 | + "type": "https://json-everything.net/errors/validation", |
| 169 | + "title": "Validation Error", |
| 170 | + "status": 400, |
| 171 | + "detail": "One or more validation errors occurred.", |
| 172 | + "errors": { |
| 173 | + "/username": [ |
| 174 | + "The value must have a minimum length of 3" |
| 175 | + ], |
| 176 | + "/email": [ |
| 177 | + "The value does not match the required pattern" |
| 178 | + ] |
| 179 | + } |
| 180 | +} |
| 181 | +``` |
| 182 | + |
| 183 | +The error paths use [JSON Pointer](https://tools.ietf.org/html/rfc6901) notation, making it easy to map errors to specific fields in your request. |
| 184 | + |
| 185 | +## How it works {#schema-api-internals} |
| 186 | + |
| 187 | +The library integrates with ASP.NET Core through three main components: |
| 188 | + |
| 189 | +### ValidatingJsonModelBinder |
| 190 | + |
| 191 | +This custom model binder intercepts the model binding process to: |
| 192 | + |
| 193 | +- Read the request body |
| 194 | +- Deserialize using the configured JSON serializer (with validation) |
| 195 | +- Capture validation errors from the `ValidatingJsonConverter` |
| 196 | +- Add errors to `ModelState` using JSON Pointer paths |
| 197 | + |
| 198 | +### JsonSchemaValidationFilter |
| 199 | + |
| 200 | +This filter implements both `IActionFilter` and `IAlwaysRunResultFilter` to: |
| 201 | + |
| 202 | +- Inspect `ModelState` for validation errors after model binding |
| 203 | +- Filter errors to only those with JSON Pointer paths (ensuring they came from schema validation) |
| 204 | +- Build a Problem Details response with structured error information |
| 205 | +- Short-circuit the request pipeline when validation errors are present |
| 206 | + |
| 207 | +### GenerativeValidatingJsonConverter |
| 208 | + |
| 209 | +This converter, provided by _JsonSchema.Net.Generation_, handles: |
| 210 | + |
| 211 | +- Generating schemas from types decorated with `[GenerateJsonSchema]` |
| 212 | +- Validating JSON during deserialization |
| 213 | +- Caching generated schemas for performance |
| 214 | +- Throwing `JsonException` with validation results when validation fails |
| 215 | + |
| 216 | +## Best practices {#schema-api-best-practices} |
| 217 | + |
| 218 | +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. |
| 219 | +2. **Test your schemas** - Use unit tests to verify that your validation attributes produce the expected schemas and validation behavior. |
| 220 | +3. **Consider separate validation models** - For complex scenarios, create dedicated request models separate from your domain entities to keep validation concerns isolated. |
| 221 | +4. **Customize error messages** - The default validation errors are descriptive, but you can provide [custom error messages](./basics#schema-errors) to better match your application's needs. |
| 222 | + |
| 223 | +## Example: Complete setup {#schema-api-example} |
| 224 | + |
| 225 | +Here's a complete example showing all the pieces together: |
| 226 | + |
| 227 | +```c# |
| 228 | +// Program.cs |
| 229 | +var builder = WebApplication.CreateBuilder(args); |
| 230 | + |
| 231 | +builder.Services.AddControllers() |
| 232 | + .AddJsonSchemaValidation(converter => |
| 233 | + { |
| 234 | + converter.GeneratorConfiguration.PropertyNameResolver = PropertyNameResolvers.CamelCase; |
| 235 | + converter.EvaluationOptions.RequireFormatValidation = true; |
| 236 | + }); |
| 237 | + |
| 238 | +var app = builder.Build(); |
| 239 | +app.MapControllers(); |
| 240 | +app.Run(); |
| 241 | + |
| 242 | +// Models/CreateProductRequest.cs |
| 243 | +[GenerateJsonSchema] |
| 244 | +[AdditionalProperties(false)] |
| 245 | +public class CreateProductRequest |
| 246 | +{ |
| 247 | + [Required] |
| 248 | + [MinLength(1)] |
| 249 | + [MaxLength(100)] |
| 250 | + public string Name { get; set; } |
| 251 | + |
| 252 | + [MinLength(10)] |
| 253 | + [MaxLength(500)] |
| 254 | + public string? Description { get; set; } |
| 255 | + |
| 256 | + [Minimum(0.01)] |
| 257 | + public decimal Price { get; set; } |
| 258 | + |
| 259 | + [MinItems(1)] |
| 260 | + [UniqueItems(true)] |
| 261 | + public List<string> Tags { get; set; } |
| 262 | +} |
| 263 | + |
| 264 | +// Controllers/ProductsController.cs |
| 265 | +[ApiController] |
| 266 | +[Route("api/[controller]")] |
| 267 | +public class ProductsController : ControllerBase |
| 268 | +{ |
| 269 | + [HttpPost] |
| 270 | + public IActionResult CreateProduct([FromBody] CreateProductRequest request) |
| 271 | + { |
| 272 | + // Fully validated request is available here |
| 273 | + return Ok(new { message = "Product created successfully" }); |
| 274 | + } |
| 275 | +} |
| 276 | +``` |
| 277 | + |
| 278 | +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. |
0 commit comments