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.
Performance
Section titled “Performance”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.
Running the benchmarks
Section titled “Running the benchmarks”# All suitesdotnet run --project src/src/Benchmarks/Benchmarks.csproj -c Release# orjust 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.
Measurement environment
Section titled “Measurement environment”- 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.
Core validation (BasicPerformanceTests)
Section titled “Core validation (BasicPerformanceTests)”| 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 |
Objects (ObjectPerformanceTests)
Section titled “Objects (ObjectPerformanceTests)”| 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 |
Arrays (ArrayPerformanceTests)
Section titled “Arrays (ArrayPerformanceTests)”| 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 |
Heavy scenarios (HeavyPerformanceTests)
Section titled “Heavy scenarios (HeavyPerformanceTests)”| 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 |
Transforms (TransformPerformanceTests)
Section titled “Transforms (TransformPerformanceTests)”| 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 |
Unions (UnionPerformanceTests)
Section titled “Unions (UnionPerformanceTests)”| 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 |
Memory (MemoryPerformanceTests)
Section titled “Memory (MemoryPerformanceTests)”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:
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’sValidateon valid and invalid input (baseline), field-levelValidateMemberon valid and invalid input, andTryResolveMemberon a nested path.ProblemDetailsMappingPerformanceTests- converting aZodExceptiontoHttpValidationProblemDetailswithout a registry, with anErrorTypeRegistrylookup, 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,IValidatableObjector refinements - so validating one Blazor field costs one member, not the whole graph. - Synchronous fast path.
{Type}Schema.RequiresAsyncValidationis a compile-time constant. When nothing in the graph is asynchronous,ValidateAsynccompletes synchronously (it returnsnew ValueTask<…>(Validate(…))). PreferValidateeven inside async code: it skips theValueTaskand 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.
TryResolveMemberis reflection-free: it walks the generated member table, so it is trim- and AOT-safe.
UUID validation (UuidPerformanceTests)
Section titled “UUID validation (UuidPerformanceTests)”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.
Optimizations that make it fast
Section titled “Optimizations that make it fast”- Struct-based rules — every rule is a
readonly record structimplementingIValidationRule<T>, so there is no per-validation object allocation. - Span-aware helpers —
Span<T>/ReadOnlySpan<T>APIs andArrayPool<T>-backed helpers. The shipped string rules implementIStringValidationRule(except the URL and Base64 rules, which need a string), soZodString.ValidateSpanvalidates the span directly and only materialises the value string for its result;ZodString.IsValidSpanavoids that allocation entirely. - Compiled validators —
CompiledValidator.Compileremoves interface dispatch (see Compiled Validators and Caching). - Source generation —
[ZodSchema]emits direct property access and typed equality checks with no reflection (see Source Generator). - Cacheable schema instances — a fully built schema is safe to cache and share, so
SchemaCacheavoids 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.