Documentation section: Hosting
Microsoft.Extensions.Validation
Prerelease. These pages describe 3.0.0-prerelease.2 and can differ from the published release. Read the Live 2.4.2 documentation instead.
Microsoft.Extensions.Validation
Section titled “Microsoft.Extensions.Validation”Purview.ZodSharp.Extensions.Validation connects ZodSharp to .NET 11’s
Microsoft.Extensions.Validation, the validation
pipeline that minimal APIs, Blazor Server and Blazor WebAssembly share. It registers ZodSharp as a validation
resolver, so every type that has a ZodSharp validator is validated by ZodSharp wherever the platform validates.
That includes nested objects, collections and dictionaries, and it works without [ValidatableType] or the
Microsoft.Extensions.Validation source generator.
- Targets
net11.0only. - References the
Microsoft.Extensions.Validationpackage, never the ASP.NET Core shared framework, so it works in a Blazor WebAssembly client and in worker or console hosts. Purview.ZodSharp.AspNetCoredepends on it onnet11.0. That package turns the pipeline’s response into the full ZodSharp ProblemDetails contract (see ASP.NET Core Integration).
Install
Section titled “Install”dotnet add package Purview.ZodSharp.Extensions.ValidationRegister
Section titled “Register”using ZodSharp.Core;
builder.Services.AddZodSharpFactory(factory => factory.Register(new OrderSchemaValidator()));builder.Services.AddZodSharpValidation();
// Keep calling AddValidation() in the application when DataAnnotations types should be validated too:// the Microsoft.Extensions.Validation generator only intercepts calls in your own code.builder.Services.AddValidation();AddZodSharpValidation(Action<ZodValidationOptions>? configure = null) does the following:
- Registers
IZodSchemaFactorywhen none is registered yet, asAddZodSharpFactory()does. Any registration works:AddZodSharpFactory,AddZodSharpfrom the ASP.NET Core package, or the generatedAddZodSharp{Assembly}Schemas()(see Source Generator). - Calls
AddValidation(). - Registers
ZodValidatableInfoResolveras a singleton. - Places the resolver in
ValidationOptions.Resolversfrom a post-configure step. The generated DataAnnotations resolver inserts itself at index 0 inside its own configure step, so placing ZodSharp afterwards keeps it wherePlacementsays. It does not matter whetherAddValidation()is called beforeAddZodSharpValidation(), after it, or not at all.
Calling AddZodSharpValidation() more than once registers the resolver once, and every configure callback is
applied.
Endpoint validation
Section titled “Endpoint validation”The endpoint filter Microsoft.Extensions.Validation adds to minimal APIs validates every handler parameter that a
registered resolver claims, before the handler runs; it is not switched on by AddValidation() itself (called from a
library, AddValidation() adds no resolver at all). So AddZodSharpValidation() turns on minimal-API validation
for every ZodSharp-typed parameter: an invalid body is answered with 400 and the handler never runs. That is
usually the point, but it applies to the whole application:
- Opt an endpoint or a route group out with
DisableValidation(). - Register type validation only with
AddZodSharpValidation(configure, validateParameters: false), or setZodValidationOptions.ValidateParameters = false: Blazor forms andValidationOptions.ValidateAsync<T>still use ZodSharp, and no endpoint changes. This is whatAddZodSharpBlazor()registers. - Any registration that asks for parameters wins (so
AddZodSharpBlazor()plusAddZodSharpValidation(), in either order, validates endpoints), and an explicitValidateParametersoverrides both.
Resolver ordering and DataAnnotations
Section titled “Resolver ordering and DataAnnotations”A type is looked up in ZodValidationOptions.Sources first, then in IZodSchemaFactory. A type found in neither
falls through to the next resolver, which is usually the generated DataAnnotations resolver.
Placement |
Effect |
|---|---|
ZodResolverPlacement.First (default) |
ZodSharp runs ahead of the generated DataAnnotations resolvers, so a type with a ZodSharp validator is always validated by ZodSharp. |
ZodResolverPlacement.AfterGenerated |
ZodSharp is placed after the generated type resolvers and ahead of the runtime parameter resolvers they append. A type the Microsoft.Extensions.Validation generator knows is validated with DataAnnotations, and ZodSharp handles the rest. |
Generated [ZodSchema] validators already apply DataAnnotations attributes, so a ZodSharp type is validated
by ZodSharp only. Set AlsoRunDataAnnotations = true to also run the next resolver that knows the type: ZodSharp
runs first, and an identical (Path, ErrorMessage) pair is reported once. Nested ZodSharp types are not
validated twice.
Options
Section titled “Options”ZodValidationOptions |
Default | Effect |
|---|---|---|
Placement |
First |
Where the resolver sits; see above. |
AlsoRunDataAnnotations |
false |
Also run the next resolver for ZodSharp types, reporting identical messages once. |
PathNamer |
null (C# member names) |
Renames member segments in ValidationError.Path, for example ZodPathNamer.FromFunc(JsonNamingPolicy.CamelCase.ConvertName). Blazor ignores Path, so this never affects field placement. |
MessageFormatter |
null |
Formats every ErrorMessage. When null, a StringLocalizerZodMessageFormatter is used whenever the validation’s service provider resolves an IStringLocalizerFactory, and the ZodSharp message otherwise. See Localization. |
Sources |
empty | Trim-safe IZodValidatableInfoSources consulted before IZodSchemaFactory. |
ValidateParameters |
null (by registration) |
Whether the resolver claims parameters, which turns on minimal-API endpoint validation for ZodSharp types; see Endpoint validation. null: claimed after AddZodSharpValidation(), not when only AddZodSharpBlazor() registered the bridge. |
using System.Text.Json;using ZodSharp.Core;using ZodSharp.Extensions.Validation;
builder.Services.AddZodSharpValidation(options =>{ options.Placement = ZodResolverPlacement.First; options.PathNamer = ZodPathNamer.FromFunc(JsonNamingPolicy.CamelCase.ConvertName);});What each error carries
Section titled “What each error carries”Each ZodSharp error becomes one Microsoft.Extensions.Validation.ValidationError:
ValidationError |
Value |
|---|---|
Path |
The canonical ZodSharp path prefixed with ValidateContext.CurrentValidationPath: Customer.Address.Street, Lines[0].Sku, ByCode[abc].Sku. See Core Concepts. |
Name |
The failing member’s name (Street); empty for a model-level error. |
Container |
The object that owns the member (order.Customer.Address). Blazor uses it to place the message on the right field. Never a struct: an error inside a struct member (Email.Value) is attributed to the nearest reference-type owner and its member (signUp, Email), because a value type cannot be a FieldIdentifier model. |
ErrorMessage |
The ZodSharp message, formatted by MessageFormatter or localized (see Localization). |
Container and Name come from the generated TryResolveMember, so no reflection is needed. Validators that are
not generated fall back to a property walker. That walk runs only when validation fails, and it never throws.
Parameters follow Microsoft.Extensions.Validation’s own conventions:
- A complex parameter is not prefixed (
Name,Address.Street). - A collection parameter is prefixed with the parameter name (
items[0].Name). - An error about the value itself (a missing body, a root refinement) is keyed by the parameter name and has no container.
A nullable or optional parameter that is null is not validated. [SkipValidation] and [FromServices]
parameters and primitive types are never claimed by ZodSharp.
Full-fidelity errors
Section titled “Full-fidelity errors”A Microsoft.Extensions.Validation error holds only a name, path, message and container. The full ZodSharp errors (code, category, origin, bounds, parameters) travel alongside on one of two side channels:
ValidateContext.ZodReportreturns theZodValidationReportattached to the context, ornullwhen ZodSharp reported nothing. The report is held weakly, so it lives exactly as long as the context.IZodValidationReportSinkis used when the validation’s service provider resolves one. The bridge appends every validated value’s errors to it as well. Register it with a lifetime equal to one validation run, which is scoped in ASP.NET Core.AddZodSharpProblemDetails()registers one (ZodRequestValidationReport) onnet11.0. Do not register a sink in Blazor Server, where the scope is the circuit and errors would accumulate.
using Microsoft.Extensions.Validation;using ZodSharp.Extensions.Validation;
ValidateContext context = new() { ValidationOptions = options, ServiceProvider = services };await typeInfo.ValidateAsync(order, context, cancellationToken);
ZodValidationReport? report = context.ZodReport;Every error in a report has a canonical FullPath that already includes CurrentValidationPath. It therefore
renders to the same key as the matching Microsoft.Extensions.Validation.ValidationError.Path.
When ZodSharp runs nested under a DataAnnotations type whose sibling members complete asynchronously, the platform
validates those members on cloned contexts and merges back only its own errors. The ZodReport can then be on a
clone. Register a sink where that matters. When ZodSharp validates the root value (minimal API parameters, Blazor
forms of ZodSharp models), the report is always on the caller’s context.
Validating outside HTTP and Blazor
Section titled “Validating outside HTTP and Blazor”ValidationOptions.ValidateAsync<T>(value, services, cancellationToken) validates through the configured resolvers
in worker services, console hosts and tests, and returns a ZodValidationReport:
using Microsoft.Extensions.Options;using Microsoft.Extensions.Validation;
var options = serviceProvider.GetRequiredService<IOptions<ValidationOptions>>().Value;var report = await options.ValidateAsync(order, serviceProvider, cancellationToken);
foreach (var error in report.Errors) Console.WriteLine($"{error.FullPath.ToDottedString()}: {error.Message} ({error.Code})");The runtime type of value is used when it differs from T. ZodSharp errors keep full fidelity. Errors from other
resolvers (DataAnnotations) are converted with code and category custom, and their path is parsed from the
Microsoft.Extensions.Validation key with ZodPath.ParseDotted.
Synchronous and asynchronous validation
Section titled “Synchronous and asynchronous validation”The bridge behaves like the platform’s own type info:
- Sync with async rules throws. Synchronous validation of a type with asynchronous rules throws
InvalidOperationException, asEditContext.Validate()does. Use the async APIs: minimal APIs already do, and in Blazor useEditContext.ValidateAsync(). A type has asynchronous rules when itsRequiresAsyncValidationistrue. - Async rules can use services. Asynchronous validation passes
ValidateContext.ServiceProviderthrough to service-aware rules (see Source Generator). - Field-level validation does not recurse. It runs only the changed member’s own rules (
ValidateMember). Nested schemas, type-level rules and refinements are not run, because Blazor attributes every error of a field run to the changed field. Members without rules are not offered for field-level validation (IsValidatableMember).
Depth limit
Section titled “Depth limit”The adapter throws InvalidOperationException when ValidateContext.CurrentDepth reaches
ValidationOptions.MaxDepth, like Microsoft.Extensions.Validation. Generated validators also recurse internally with
their own guard, ZodValidationDepth.MaxDepth (default 32). The bridge sets that value from
ValidationOptions.MaxDepth, so a cyclic graph fails at the same depth however it is validated.
Unions
Section titled “Unions”A C# union that has no validator of its own is validated through its active case, the way
Microsoft.Extensions.Validation does. The case is reported under Value (Value.Name, Value[0].Name) and
validated by whichever resolver knows the case type. For a union minimal-API parameter, the runtime parameter
resolver that the application’s own AddValidation() call adds claims the parameter. See
Unions and Discriminated Unions.
Trim-safe sources
Section titled “Trim-safe sources”ZodValidationOptions.Sources supplies validators by type without the factory:
using ZodSharp.Core;using ZodSharp.Extensions.Validation;
sealed class OrderValidators : IZodValidatableInfoSource{ public bool TryGetValidator(Type type, out IZodUntypedValidator? validator) { validator = type == typeof(Order) ? new OrderSchemaValidator() : null; return validator is not null; }}
builder.Services.AddZodSharpValidation(options => options.Sources.Add(new OrderValidators()));Generated {Type}SchemaValidators implement IZodUntypedValidator. For a hand-written
IZodSchemaValidator<T>, wrap it in new ZodUntypedValidator<T>(validator). The generated per-assembly
registration (AddZodSharp{Assembly}Schemas()) is also trim-safe. Assembly scanning is not.
Limitations
Section titled “Limitations”- Runtime dictionary schemas have no owning object. A runtime
Z.Object()schema validating a dictionary or JSON value has no CLR object a field can be bound to. Its errors carryContainer = nulland the full path asName, so Blazor shows them in the validation summary rather than next to a field. - Models declared in
.razorfiles are not generated. The Razor compiler generates C# from@codeblocks after source generators run, so the ZodSharp generator never sees a[ZodSchema]type declared there. The generator reportsZODSGEN051; move the type to a.csfile (see Source Generator Diagnostics). - Lookups are cached. Register validators before the first validation (see above).
See also
Section titled “See also”- ASP.NET Core Integration: the minimal-API pipeline and the ProblemDetails contract.
- Blazor Integration: forms, field placement and
<ZodValidator />. - OpenAPI Integration
- Localization
- Core Concepts: the error fields and the canonical path format.