ASP.NET Core Integration
ASP.NET Core Integration
Section titled “ASP.NET Core Integration”The Purview.ZodSharp.AspNetCore package converts failed validation results into standard ProblemDetails / HttpValidationProblemDetails payloads while preserving the structured validation issues. It also registers Purview.ZodSharp schema resolution into your application’s dependency injection container.
Install
Section titled “Install”dotnet add package Purview.ZodSharp.AspNetCoreProblemDetails
Section titled “ProblemDetails”using ZodSharp.AspNetCore;
var result = BasketSchema.Validate(basket);
if (!result.IsSuccess){ var problem = result.ToHttpValidationProblemDetails(); return Results.ValidationProblem( problem.Errors, extensions: new Dictionary<string, object?> { ["issues"] = problem.Extensions["issues"], });}| Member | Behaviour |
|---|---|
ToHttpValidationProblemDetails<T>(ValidationResult<T> result, int statusCode = 400) |
HttpValidationProblemDetails; error paths flattened to dotted keys (user.email, array indexes as [0]) |
ToValidationProblemDetails<T>(ValidationResult<T> result, int statusCode = 400) |
ValidationProblemDetails |
Both throw InvalidOperationException when the result IsSuccess. The structured metadata is preserved in the issues extension as a ValidationIssue[]:
public sealed class ValidationIssue{ public required string Code { get; init; } public string? Category { get; init; } public string? Origin { get; init; } public int? Minimum { get; init; } public int? Maximum { get; init; } public bool? Inclusive { get; init; } public required string[] Path { get; init; } public required string Message { get; init; } public IReadOnlyDictionary<string, object?>? Parameters { get; init; }}Minimal API validation
Section titled “Minimal API validation”Use WithZodSharpValidation<T>() to validate the request DTO at the endpoint and short-circuit to a standard
validation-problem response, or ToValidationProblem() to return a Results.ValidationProblem IResult
directly from a handler:
using Microsoft.AspNetCore.Builder;using ZodSharp;using ZodSharp.AspNetCore;
// Validate the bound request DTO automatically (requires AddZodSharp to register the validator):app.MapPost("/users", (UserDto dto) => dto) .WithZodSharpValidation<UserDto>();
// Or validate manually and return a minimal-API IResult:app.MapPost("/users", (UserDto dto) =>{ var result = UserDtoSchema.Validate(dto); return result.IsSuccess ? TypedResults.Ok(result.Value) : result.ToValidationProblem();});WithZodSharpValidation<T>() adds an endpoint filter that validates the first bound argument of type T
against the IZodSchemaFactory registered by AddZodSharp (or AddZodSharpFactory). On failure it returns
the same HttpValidationProblemDetails payload produced by ToHttpValidationProblemDetails(); on success it
passes through to the handler. ToValidationProblem() is the handler-side equivalent, returning a status-400
Results.ValidationProblem IResult.
Exception handling
Section titled “Exception handling”A thrown ZodException (for example from Parse, GetValueOrThrow(), or a value object’s generated
Create under strict deserialization) can be mapped to ProblemDetails on demand:
using ZodSharp.AspNetCore;
try{ var parsed = EmailAddressSchema.Parse(rawValue);}catch (ZodException ex){ return Results.ValidationProblem(ex.ToHttpValidationProblemDetails().Errors);}Or handled automatically by an IExceptionHandler. Register it and ensure UseExceptionHandler() is in
the pipeline:
builder.Services.AddZodSharpProblemDetails();
var app = builder.Build();
app.UseExceptionHandler();AddZodSharpProblemDetails(Action<ZodProblemDetailsOptions>? configure = null, Action<ProblemDetailsOptions>? problemDetails = null)
registers ZodExceptionHandler, calls services.AddProblemDetails(problemDetails), and configures
ZodProblemDetailsOptions:
ErrorTypeRegistry Registry— resolves error codes toErrorTypes. Defaults toErrorTypeRegistry.Default.bool FormatMessages— whentrue, messages are formatted from the matchedErrorType.MessageFormat.Func<ImmutableArray<ValidationError>, int>? StatusCodeSelector— an escape hatch that takes complete control of the response status code.
Mapping error types to status codes
Section titled “Mapping error types to status codes”The ErrorType factory lives in the core Purview.ZodSharp package: ErrorType, the
[ErrorType] attribute, the source generator that emits Create/Throw helpers, and the
ZODSASP001/ZODSASP002/ZODSASP003 analyzers are all shipped with it and are usable without any
ASP.NET Core dependency. The Purview.ZodSharp.AspNetCore package adds the ErrorTypeRegistry
and the ProblemDetails mapping on top.
Register an ErrorType (code, optional category, description, HTTP status, and optional message
template) in a registry, then let the mapper derive the status code, title, detail, and formatted
messages automatically:
using ZodSharp.AspNetCore;using ZodSharp.Core;
public static partial class ConcurrentErrorType{ [ErrorType] public static readonly ErrorType SaveFailed = new( Code: "aggregate_save_failed", Category: "invalid_value", Description: "The aggregate could not be saved.", HttpStatus: 409, MessageFormat: "Aggregate '{AggregateId}' (of type {AggregateType}) failed to save", Parameters: [ new("AggregateId", typeof(string)), new("AggregateType", typeof(string)) ]);}
// Register once at startup:ErrorTypeRegistry.Default.Register(ConcurrentErrorType.SaveFailed);HttpStatus is kept on ErrorType purely for convenience; the core library does not use it — only the
ASP.NET Core integration reads it when building ProblemDetails responses. The optional Category is a
broad grouping that can span many specific codes (for example code invalid_tenant_id with category
invalid_value); the generated helpers copy it onto each ValidationError, and it is surfaced on the
serialized ValidationIssue in the issues extension.
Parameters can also be declared with the ErrorType.Param<T>("Name") helper instead of an explicit
typeof(...):
Parameters:[ new("AggregateId", typeof(string)), ErrorType.Param<string>("AggregateType")]Both the bundled ZODSASP001 analyzer and the source generator (shipped with the core package)
treat ErrorType.Param<T> entries exactly like any other declared parameter, so placeholder checking
and helper generation are unchanged.
When an error carries that code, the response status, title, and message are derived automatically:
throw new ZodException([ ValidationError.Create( "aggregate_save_failed", "The aggregate could not be saved.", path: [], parameters: new Dictionary<string, object?> { ["AggregateId"] = "agg-123", ["AggregateType"] = "Invoice", }),]);Generated Create / Throw helpers
Section titled “Generated Create / Throw helpers”Marking the field with [ErrorType] and the containing class partial lets the source generator
bundled with the core package turn each field into strongly typed static helpers. For the field above
it generates ConcurrentErrorType.CreateSaveFailed(...) and
ConcurrentErrorType.ThrowSaveFailed(...) with one strongly typed parameter per entry in
Parameters (the declared typeof(...) type, or the ErrorType.Param<T> generic type argument):
// Returns a ValidationError with the code, the formatted message, and the typed parameters.var error = ConcurrentErrorType.CreateSaveFailed("agg-123", "Invoice");
// Throws a ZodException carrying the same ValidationError.ConcurrentErrorType.ThrowSaveFailed("agg-123", "Invoice");
// The path and structured issue metadata can be populated too:var error = ConcurrentErrorType.CreateSaveFailed( "agg-123", "Invoice", path: ["order", "items", "[0]"], origin: "collection", minimum: 1, maximum: 10, inclusive: true);The generated Create builds a typed ErrorTypeParameters instance (validated against the declared
parameter types and exposed through ValidationError.Parameters) and sets the message from
ErrorType.FormatMessage, so error.Message already reads
Aggregate 'agg-123' (of type Invoice) failed to save and mapping through the registry produces the
409 Conflict response described below.
ErrorType diagnostics
Section titled “ErrorType diagnostics”The ErrorType factory, its source generator, and its analyzers ship with the core Purview.ZodSharp package:
| ID | Severity | Meaning |
|---|---|---|
ZODSASP001 |
Warning | A MessageFormat placeholder is not declared in ErrorType.Parameters |
ZODSASP002 |
Warning | The containing type of an [ErrorType] field is not declared partial |
ZODSASP003 |
Warning | An [ErrorType] field is not declared static readonly |
ZODSASP100 |
Error | Unhandled exception in the ErrorType source generator |
ZODSASP101 |
Error | The Parameters of an [ErrorType] field could not be extracted |
ZODSASP001–ZODSASP003 explain why Create/Throw helpers were not generated; ZODSASP100/ZODSASP101 are fatal generator failures that name the field they failed on.
Produces a 409 Conflict HttpValidationProblemDetails with:
{ "status": 409, "detail": "The aggregate could not be saved.", "errors": { "": ["Aggregate 'agg-123' (of type Invoice) failed to save"] }, "traceId": "...", "aggregateId": "agg-123", "issues": [ { "code": "aggregate_save_failed", "path": [], "message": "Aggregate 'agg-123' (of type Invoice) failed to save", "parameters": { "AggregateId": "agg-123", "AggregateType": "Invoice" } } ]}Mapping rules:
- Status — the highest matched
ErrorType.HttpStatuswins; unmapped codes fall back to the default (400). Override withZodProblemDetailsOptions.StatusCodeSelector. - Title / Type / Detail — taken from the highest-status matched
ErrorType; otherwise defaulted. - Message —
MessageFormatnamed placeholders (for example{AggregateId}) are substituted fromValidationError.Parameters. Placeholders without a matching value are left as-is so templating gaps stay visible. The analyzerZODSASP001(shipped with the core package) warns at compile time when aMessageFormatplaceholder is not declared inParameters. - Parameters — the error’s
ValidationError.Parametersare surfaced both per-issue in theissuesextension and merged (camel-cased) into the top-level ProblemDetails extensions for client correlation.
ToHttpValidationProblemDetails / ToValidationProblemDetails accept an ErrorTypeRegistry or a
Func<string, ErrorType?> lookup for on-demand mapping, and the same mapping applies to
ValidationResult<T>.
Dependency injection
Section titled “Dependency injection”builder.Services.AddZodSharp(options =>{ options.ScanAssemblies.Add(typeof(UserDto).Assembly); options.ScanAssemblyGraphs.Add(typeof(Program).Assembly); options.ScanLoadedAssemblies = true;});AddZodSharp(Action<ZodSchemaFactoryOptions>? configure) registers IZodSchemaFactory as a singleton, applies options.ConfigureFactory, and calls factory.RegisterFromAssembly(assembly) for each distinct assembly discovered from the configured assembly sources. This auto-discovers source-generated [assembly: ZodSchemaGenerated(typeof(...))] registrations.
ZodSchemaFactoryOptions:
List<Assembly> ScanAssemblies— assemblies to scan for generated schemas.List<Assembly> ScanAssemblyGraphs— root assemblies whose referenced assembly graphs should be scanned for generated schemas.bool ScanLoadedAssemblies— whether to scan all currently loaded assemblies for generated schemas.Action<IZodSchemaFactory>? ConfigureFactory— additional factory configuration.
For modular registration, the package also exposes additive assembly-contribution helpers:
builder.Services.AddZodSharp();builder.Services.AddZodSharpAssembly(typeof(UserDto).Assembly);builder.Services.AddZodSharpAssemblyGraph(typeof(Program).Assembly);builder.Services.AddZodSharpLoadedAssemblies();AddZodSharpAssembly(Assembly)contributes an exact assembly scan.AddZodSharpAssemblyGraph(Assembly)contributes the root assembly plus its referenced assemblies.AddZodSharpLoadedAssemblies()contributes a scan across assemblies currently loaded into the application domain.
Choosing a registration method
Section titled “Choosing a registration method”| Method | Package | Registers | Use when |
|---|---|---|---|
AddZodSharp(options) |
Purview.ZodSharp.AspNetCore |
singleton IZodSchemaFactory + auto-registers generated validators from configured assembly sources |
ASP.NET Core apps that want factory registration plus optional assembly-source configuration. |
AddZodSharpAssembly(...), AddZodSharpAssemblyGraph(...), AddZodSharpLoadedAssemblies() |
Purview.ZodSharp.AspNetCore |
additive generated-validator assembly source contributions | Modular ASP.NET Core apps where deeper layers or implementation packages contribute their own schema assemblies. |
AddZodSharpFactory(configure) |
core Purview.ZodSharp |
singleton IZodSchemaFactory |
Any .NET host where you want manual control — register validators yourself in the configure callback. |
AddZodSharpProblemDetails(...) |
Purview.ZodSharp.AspNetCore |
ZodExceptionHandler + ProblemDetails services only — does not register the factory |
Mapping thrown ZodExceptions to ProblemDetails; pair it with AddZodSharp when you also need DI validator resolution. |
AddZodSchemaOptionsValidator<T>(...) |
core Purview.ZodSharp |
singleton IValidateOptions<T> |
Validating options objects; requires a factory registered first via AddZodSharp or AddZodSharpFactory. |
See Dependency Injection for the underlying factory and options-validation wiring.