Skip to content

Commit 55e9461

Browse files
authored
Merge pull request #26 from json-everything/schema/api
add schema.api docs
2 parents fb0a543 + 35f2847 commit 55e9461

2 files changed

Lines changed: 278 additions & 0 deletions

File tree

.jekyll-metadata

1.5 KB
Binary file not shown.

_docs/schema/api-validation.md

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
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

Comments
 (0)