Skip to content
Purview Dev logo

Documentation section: Quality

Performance

Prerelease. These pages describe 3.0.0-prerelease.2 and can differ from the published release. Read the Live 2.4.2 documentation instead.

Stable ZodSharp Reviewed 2026-11-03 purview-dev/zodsharp Star on GitHub 2 aspnetcore c-sharp csharp dotnet json-schema newtonsoft-json schema source-generator system-text-json validation zero-allocation zod

Purview.ZodSharp is designed for maximum performance: validation rules are readonly record structs, hot paths use Span<T>, and the source generator emits direct typed codegen with no reflection. The committed BenchmarkDotNet suite measures every scenario.

Terminal window
# All suites
dotnet run --project src/src/Benchmarks/Benchmarks.csproj -c Release
# or
just perf-tests
# A specific suite (the `--` passes the filter to BenchmarkDotNet)
dotnet run --project src/src/Benchmarks/Benchmarks.csproj -c Release -- --filter "*ObjectPerformanceTests*"

Results are written to BenchmarkDotNet.Artifacts/ (HTML, Markdown, logs) in the project directory. Use -c Release; the suite uses [MemoryDiagnoser] and a [SimpleJob] profile.

  • BenchmarkDotNet 0.15.8, .NET 10.0.12, Windows 11 (10.0.28020.2991).
  • These figures are stale in one respect: the repository now pins BenchmarkDotNet 0.16.0-preview.2 (Directory.Packages.props), so the numbers below were produced by a different version than the suite currently builds against. They are also from one machine. Re-run the suite before relying on absolute values; use them for relative comparison between scenarios.
  • 13th Gen Intel Core i9-13900KF 3.00 GHz (24 physical / 32 logical cores), X64 RyuJIT x86-64-v3.

Numbers are indicative; re-run on your own hardware for local planning.

Scenario Mean Allocated
ValidateBoolean 2.170 ns 0 B
ValidateNumber 10.702 ns 0 B
ValidateString 41.627 ns 0 B
ValidateStringArray 57.031 ns 0 B
ValidateStringWithMultipleRules 77.313 ns 0 B
ValidateNumberWithMultipleRules 16.427 ns 0 B
Scenario Mean Allocated
ValidateSimpleObject (2 fields) 89.78 ns 0 B
ValidateMediumObject (6 fields) 340.18 ns 0 B
ValidateComplexObject (13 fields, nested) 795.69 ns 0 B
ValidateComplexObjectInvalid 1,152.80 ns 1,960 B
Scenario Mean Allocated
ValidateSmallArray 124.4 ns —
ValidateLargeArray (1000 items) 11,682.3 ns —
ValidateMediumArray (100 items) 4,585.3 ns —
ValidateNumberArray 12,797.3 ns —
ValidateLargeArrayWithComplexSchema 8,011.8 ns —
ValidateLargeArrayInvalid 11,206.7 ns 904 B
Scenario Mean
ValidateDeepNestedObject (4 levels) 198.5 ns
ValidateWideObject (50 fields) 2,217.2 ns
ValidateNestedArray 177.7 ns
ValidateStringWithManyRefinements 114.7 ns
ValidateLargeObjectWithArrays 19,578.0 ns
Scenario Mean Allocated
TransformToLower 22.26 ns 48 B
TransformToUpper 28.39 ns 48 B
TransformTrim 29.10 ns 48 B
TransformChained 40.71 ns 96 B
TransformWithValidation 78.79 ns 112 B
Scenario Mean Allocated
ValidateUnion_String (first option) 48.70 ns 0 B
ValidateUnion_Number (second option) 69.38 ns 592 B
ValidateUnion_Boolean (third option) 99.71 ns 792 B
ValidateDiscriminatedUnion_FirstOption 169.33 ns 0 B
ValidateDiscriminatedUnion_SecondOption 171.90 ns 0 B
ValidateUnion_Invalid 195.87 ns 1,360 B

All valid-input paths for the core schema types are zero-allocation:

Scenario Mean Ratio
ValidateString_Allocations (baseline) 45.82 ns 1.00
ValidateObject_Allocations 94.28 ns 2.06
ValidateArray_Allocations 1,180.24 ns 25.82

Generated validators and ProblemDetails mapping

Section titled “Generated validators and ProblemDetails mapping”

Two suites cover the .NET 11 validation work. They have no committed results yet; run them locally:

Terminal window
dotnet run --project src/src/Benchmarks/Benchmarks.csproj -c Release -- --filter "*GeneratedValidationPerformanceTests*"
dotnet run --project src/src/Benchmarks/Benchmarks.csproj -c Release -- --filter "*ProblemDetailsMappingPerformanceTests*"
  • GeneratedValidationPerformanceTests - a [ZodSchema] model’s Validate on valid and invalid input (baseline), field-level ValidateMember on valid and invalid input, and TryResolveMember on a nested path.
  • ProblemDetailsMappingPerformanceTests - converting a ZodException to HttpValidationProblemDetails without a registry, with an ErrorTypeRegistry lookup, and with registry formatting.

What the design guarantees regardless of the numbers:

  • Field-level validation does not recurse. {Type}Schema.ValidateMember(value, memberName) runs only that member’s own rules - no nested schemas, type rules, IValidatableObject or refinements - so validating one Blazor field costs one member, not the whole graph.
  • Synchronous fast path. {Type}Schema.RequiresAsyncValidation is a compile-time constant. When nothing in the graph is asynchronous, ValidateAsync completes synchronously (it returns new ValueTask<…>(Validate(…))). Prefer Validate even inside async code: it skips the ValueTask and costs nothing, and CA1849 is suppressed for it (ZODSUP001).
  • Errors cost on failure only. Paths, bounds and parameters are built when a rule fails; messages are formatted with the invariant culture at validation time and re-formatted for a user’s culture only at a host boundary, so success stays allocation-free.
  • TryResolveMember is reflection-free: it walks the generated member table, so it is trim- and AOT-safe.

UUID validation uses a zero-allocation char-scan (version nibble at position 14, variant nibble at position 19) instead of a regex. Measured against the previous compiled regex:

Scenario Mean Allocated
Rule_CharScan_Valid (.UUID()) 21.99 ns 0 B
Rule_LegacyRegex_Valid (previous implementation) 27.50 ns 0 B
Rule_CharScan_Invalid < 1 ns 0 B
Rule_LegacyRegex_Invalid 14.22 ns 0 B
Rule_CharScan_Nil 20.67 ns 0 B
Rule_CharScanV7_Valid (.UUID(UuidVersion.V7)) 20.89 ns 0 B
Rule_CharScanV7_Mismatch 20.11 ns 0 B
Schema_UUID_Valid 29.80 ns 0 B
Schema_UUIDV7_Valid 26.64 ns 0 B

The char-scan is ~20% faster than the previous regex on the valid path, is version-aware at no extra cost, and rejects wrong-length strings in under a nanosecond.

  1. Struct-based rules — every rule is a readonly record struct implementing IValidationRule<T>, so there is no per-validation object allocation.
  2. Span-aware helpers — Span<T>/ReadOnlySpan<T> APIs and ArrayPool<T>-backed helpers. The shipped string rules implement IStringValidationRule (except the URL and Base64 rules, which need a string), so ZodString.ValidateSpan validates the span directly and only materialises the value string for its result; ZodString.IsValidSpan avoids that allocation entirely.
  3. Compiled validators — CompiledValidator.Compile removes interface dispatch (see Compiled Validators and Caching).
  4. Source generation — [ZodSchema] emits direct property access and typed equality checks with no reflection (see Source Generator).
  5. Cacheable schema instances — a fully built schema is safe to cache and share, so SchemaCache avoids repeated construction (see Compiled Validators and Caching). Building is not immutable: the fluent rule methods mutate the receiver. See Custom Rules for extending rules.

The allocations on a successful validation are limited to the string transforms (ToLower/ToUpper/Trim), ZodString.ValidateSpan (its result carries a string), and the union non-first-option paths.