Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/wiki/Core-Concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ ValidationResult<TOutput> result = schema.Validate(value);
| `Validate(TInput value)` | Returns a `ValidationResult<TOutput>`. Never throws. |
| `SafeParse(TInput value)` | Alias of `Validate`. |
| `Parse(TInput value)` | Returns the validated `TOutput`, or throws `ZodException` on failure (via `GetValueOrThrow()`). |
| `ValidateAsync(TInput value, CancellationToken)` | `ValueTask` wrapper around `Validate`. The pipeline is synchronous; this exists for interface symmetry and the source generator's custom async validation. |
| `ValidateAsync(TInput value, CancellationToken)` | `ValueTask` wrapper around `Validate`. The pipeline is synchronous; the token is observed before validation and throws `OperationCanceledException` when already cancelled. Genuinely async work only happens in the source generator's custom async validation. |

## ValidationResult<T>

Expand Down Expand Up @@ -64,7 +64,8 @@ catch (ZodException ex)

- Schemas are classes deriving from `ZodType<TOutput, TInput>`; the fluent methods return `this` (or a wrapping schema) so chains read naturally.
- Rules are `readonly record struct` implementations of `IValidationRule<T>` (`bool IsValid(in T value)`, `string GetErrorMessage(in T value)`) — zero allocation.
- `ValidateSpan(ReadOnlySpan<char> value)` is available on `ZodString` for span-based validation.
- Custom rules can be attached with the public `AddRule`/`Rule` methods and surfaced as DataAnnotations-style attributes; see [Custom Rules](Custom-Rules.md).
- `ValidateSpan(ReadOnlySpan<char> value)` is available on `ZodString` for span-based validation; use `IsValidSpan(value, out errors)` for an allocation-free check.
- A schema's `Description` is set with `.Describe("...")`.

## Composition model
Expand Down
377 changes: 377 additions & 0 deletions docs/wiki/Custom-Rules.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion docs/wiki/Fluent-Schema-API.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ Each schema type has its own page:

- `IZodSchema<TOutput, TInput>` — `Validate` / `ValidateAsync`; `IZodSchema<T>` is the convenience form where input equals output.
- `IZodSchemaValidator` (marker) and `IZodSchemaValidator<T>` — the DI-facing adapter surface (see [Dependency Injection](Dependency-Injection.md)).
- `IValidationRule<T>` — the rule contract implemented by every struct rule.
- `IValidationRule<T>` — the rule contract implemented by every struct rule. `ZodType<TOutput, TInput>.AddRule(rule)` and `Rule<TRule>(rule)` are public, so custom rules can be attached to any schema. `ZodString` also exposes `IsValidSpan`/`ValidateSpan` for span-based string validation, and string rules that implement `IStringValidationRule` participate in the span path.
- `IZodRule` — implemented by rules that own their error identity (`Code`/`Origin`). When a mapped rule implements it, the generator prefers the rule's values over the attribute's, so one attribute can produce a per-member error code.
- `IStringValidationRule` — the span-based counterpart of `IValidationRule<string>`.

See [Custom Rules](Custom-Rules.md) for defining, attaching, and mapping rules (including generic rules).

## Convenience composition on any schema

Expand Down
50 changes: 27 additions & 23 deletions docs/wiki/Guarantees-and-Limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,63 @@

## Guarantees

- **Zero-allocation on valid inputs.** Primitives, strings, arrays, objects, discriminated unions, and first-option unions validate without allocating when the input is valid. See [Performance](Performance.md).
- **No reflection on hot paths.** The runtime library uses expression trees only in the opt-in `CompiledValidator`; the source generator emits direct typed codegen.
- **Deterministic, reviewable generated code.** The `[ZodSchema]` generator output is stable and de-duplicated; no scope leaks in emitted code.
- **Zero-allocation on valid inputs for the core schema types.** Primitives, strings, arrays, and objects (and their generated `[ZodSchema]` validators) validate valid inputs without allocating. See [Performance](Performance.md) for the measurements. The exceptions are listed under [Limitations](#limitations).
- **No reflection on hot paths.** The runtime library uses expression trees only in the opt-in `CompiledValidator` and to compile a one-off discriminator accessor per (type, discriminator) pair for `ZodDiscriminatedUnion`. After that first use, validation runs direct property access; the source generator emits direct typed codegen.
- **Deterministic, reviewable generated code.** The `[ZodSchema]` generator output is stable and de-duplicated; there are no scope leaks in emitted code.
- **Cross-platform parity.** The C# implementation is exercised against TypeScript/Zod fixtures (see [Cross-Platform Interop](Cross-Platform-Interop.md)).
- **Multi-targeting.** Packages target `net8.0`, `net9.0`, and `net10.0`; the source generator targets `netstandard2.0` so it runs in any compiler host.
- **Immutable, shareable schemas.** Composing returns new schemas; schemas are safe to cache and share across threads.
- **A fully built schema is safe to cache and share across threads.** Validation only reads the rule set and `Description`, so once construction is finished a schema can be reused concurrently. Building is *not* immutable — see the next section.

## Limitations

### Attribute flags that are not yet honoured
### Fluent rule methods mutate the receiver

`SchemaName`, `GenerateValidateMethod`, and `GenerateParseMethod` on `[ZodSchema]` are parsed but ignored: the schema class is always `{TypeName}Schema` and `Validate`/`Parse` are always emitted. `EnableComposition` and the `IValidateOptions` flags are honoured.
`Min`, `Max`, `Email`, `Regex`, `UUID`, `StartsWith`, `EndsWith`, `Describe`, and the other fluent rule/description methods append to the receiver's rule list (or set its description) **in place** and return `this`. Only the composition methods (`Transform`, `Refine`, `SuperRefine`, `Pipe`, `Catch`, `Default`, `Prefault`, `And`, `Or`) return a new schema. Finish configuring a schema before sharing it; two pieces of code holding the same instance share its rules.

### Generated size-failure Origin
### Span validation returns a string

The generator reports `Origin = "array"` for both arrays and collections — there is no `"collection"` origin in generated code, even though `ValidationError.Origin` supports it.
`ZodString.ValidateSpan(ReadOnlySpan<char>)` validates the span directly whenever every accumulated rule implements `IStringValidationRule`, but its result carries a `string`, so a **successful** validation still allocates once when the value is materialised. Use `ZodString.IsValidSpan(ReadOnlySpan<char>, out ImmutableArray<ValidationError>)` when the value is not needed: it is allocation-free on success and only materialises the input when a rule (or a transform) cannot be evaluated over a span. `ValidateSpan` falls back to the string pipeline for schemas that involve string transforms (`Trim`, `ToLower`, `ToUpper`) or rules without a span implementation.

### Async validation is synchronous underneath
### Rules without a span implementation

`ValidateAsync` wraps the synchronous `Validate` pipeline in a `ValueTask`. The only genuinely async path is the source generator's custom async validation method (`CustomValidationAsync`), which is awaited after the sync validation.
`UrlRule` (its `Uri.TryCreate` fallback needs a string) and `Base64StringRule` (`Convert.FromBase64String`) do not implement `IStringValidationRule`; adding either to a `ZodString` disables the span fast path for that schema, and `ValidateSpan`/`IsValidSpan` fall back to materialising the input once.

### JSON Schema import scope
### Size-failure Origin values

`Z.FromJsonSchema` supports **local** `$ref` (`#/...`) references only; external `$ref` targets throw `NotSupportedException`. `FromJsonSchemaOptions` is currently empty (reserved for future options).

### Referencing both JSON integration packages

`Purview.ZodSharp.SystemTextJson` and `Purview.ZodSharp.NewtonsoftJson` both declare types with identical full names (`ZodSharp.ZExtensions`, `ZodSharp.JsonSchema.FromJsonSchemaOptions`, `FromJsonSchemaParser`, `JsonSchemaSerializerOptions`). Reference one JSON integration package; referencing both requires `extern alias`.
The generator reports `Origin = "string"` for string size failures, `Origin = "array"` for arrays, and `Origin = "collection"` for countable/`IEnumerable` collections. `ZodArray` reports `"array"`.

### Typed union allocation

`ZodTypedUnion`/`ZodUnion` allocate while attempting non-matching options and on failure. Discriminated unions dispatch directly and stay zero-allocation.
`ZodTypedUnion`/`ZodUnion` allocate while attempting non-matching options and on failure; discriminated unions backed by dictionaries dispatch directly and stay zero-allocation.

### Rule errors

Rules evaluated by the base `Validate` pipeline produce `validation_failed` errors with an empty path. Structured `too_small`/`too_big` issues (with `Minimum`/`Maximum`/`Inclusive`) come from the array schema and the source generator's size validators.
Rules evaluated by the base `Validate` pipeline produce `validation_failed` errors with an empty path. Structured `too_small`/`too_big` issues (with `Origin`, `Minimum`/`Maximum`, and `Inclusive`) are produced by `ZodArray` and by the source generator's size validators.

### String transforms allocate

`ToLower`, `ToUpper`, and `Trim` produce new strings on every validation (transform outputs are new strings by nature).

### `IStringValidationRule`

The span-based `IStringValidationRule` interface is declared but not implemented by any shipped rule struct; span validation is available through `ZodString.ValidateSpan`.

### Number semantics

`ZodNumber` operates on `double`. `Int()`, `Safe()`, and `Finite()` are validation rules, not conversions; `.Int()` rejects fractional values rather than rounding them. `MultipleOf` uses a tolerance-based comparison and rejects a zero divisor.
`ZodNumber` operates on `double`. `Int()`, `Safe()`, and `Finite()` are validation rules, not conversions; `.Int()` rejects fractional values rather than rounding them. `Positive()`/`Negative()` are strict (they reject `0`; use `NonNegative()`/`NonPositive()` for inclusive bounds). `MultipleOf` compares the quotient to its nearest integer with a relative tolerance (`1e-12`), so `0.3` is accepted for `MultipleOf(0.1)` while `0.3000000001` is not; NaN and infinity are rejected, and a zero divisor throws `ArgumentException`.

### Enum semantics

`ZodEnum` (string values) and `ZodNativeEnum<TEnum>` validate against defined members; they do not parse or convert values.

### JSON Schema import scope

`Z.FromJsonSchema` supports **local** `$ref` (`#/...`) references only; external `$ref` targets throw `NotSupportedException`. `FromJsonSchemaOptions` is currently empty (reserved for future options).

### Referencing both JSON integration packages

`Purview.ZodSharp.SystemTextJson` and `Purview.ZodSharp.NewtonsoftJson` both declare types with identical full names (`ZodSharp.ZExtensions`, `ZodSharp.JsonSchema.FromJsonSchemaOptions`, `FromJsonSchemaParser`, `JsonSchemaSerializerOptions`). Reference one JSON integration package; referencing both requires `extern alias`.

## Custom rules

Custom rules and their DataAnnotations-style attributes are a first-class extension point. Rules can be attached to a property or to the schema type itself (validating the value object as a unit), and a generic rule can be closed with the target type so one rule serves every scalar of a given shape. See [Custom Rules](Custom-Rules.md) for the rule contract, the public `AddRule`/`Rule` API, and how to map a rule to a `ValidationAttribute` that the source generator honours.

## Contract vs. underlying libraries

- `System.ComponentModel.DataAnnotations` semantics are honoured where documented — e.g. `[Length]` treats `null` as valid unless `[Required]` is present; `[RegularExpression]` runs only on non-empty strings.
Expand Down
1 change: 1 addition & 0 deletions docs/wiki/Home.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ This wiki is the project documentation hub for the core API, source generator, J
- [Arrays and Other Schemas](Arrays-and-Other-Schemas.md)
- [Unions and Discriminated Unions](Unions-and-Discriminated-Unions.md)
- [Composition and Transforms](Composition-and-Transforms.md)
- [Custom Rules](Custom-Rules.md)
- [Compiled Validators and Caching](Compiled-Validators-and-Caching.md)
- [Dependency Injection](Dependency-Injection.md)

Expand Down
15 changes: 10 additions & 5 deletions docs/wiki/Number-Validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,24 @@ var result = schema.Validate(30.0);
| `Min` | `Min(double minValue)` | `MinValueRule<double>` — `Value must be at least ...` |
| `Max` | `Max(double maxValue)` | `MaxValueRule<double>` |
| `Int` | `Int()` | `IntRule` — `value == Math.Truncate(value)` |
| `Positive` | `Positive()` | `MinValueRule<double>(0.0)` |
| `Negative` | `Negative()` | `MaxValueRule<double>(0.0)` |
| `MultipleOf` | `MultipleOf(double divisor, string? message)` | `MultipleOfRule` — throws `ArgumentException` for a zero divisor; tolerance-based |
| `Positive` | `Positive()` | `GreaterThanRule<double>(0.0)` — strictly greater than zero |
| `Negative` | `Negative()` | `LessThanRule<double>(0.0)` — strictly less than zero |
| `NonNegative` | `NonNegative()` | `MinValueRule<double>(0.0)` — greater than or equal to zero |
| `NonPositive` | `NonPositive()` | `MaxValueRule<double>(0.0)` — less than or equal to zero |
| `MultipleOf` | `MultipleOf(double divisor, string? message)` | `MultipleOfRule` — throws `ArgumentException` for a zero divisor; relative-tolerance comparison (`1e-12`) |
| `Finite` | `Finite(string? message)` | `FiniteRule` — `double.IsFinite` |
| `Safe` | `Safe(string? message)` | `SafeIntegerRule` — integer within `int.MinValue`..`int.MaxValue` |

## Examples

```csharp
var positive = Z.Number().Positive();
var negative = Z.Number().Negative();
var positive = Z.Number().Positive(); // > 0
var negative = Z.Number().Negative(); // < 0
var nonNegative = Z.Number().NonNegative(); // >= 0
var nonPositive = Z.Number().NonPositive(); // <= 0

var multipleOf = Z.Number().MultipleOf(10); // multiples of 10
var fractional = Z.Number().MultipleOf(0.1); // 0.3 is accepted (floating-point tolerance)
var finite = Z.Number().Finite(); // rejects Infinity / NaN
var safe = Z.Number().Safe(); // safe integer range
var whole = Z.Number().Int(); // no fractional part
Expand Down
11 changes: 7 additions & 4 deletions docs/wiki/Performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,17 @@ Numbers are indicative; re-run on your own hardware for local planning.

## Memory (`MemoryPerformanceTests`)

All valid-input paths are zero-allocation:
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 |

> [!NOTE]
> The string transforms, `ZodString.ValidateSpan` (its result carries a string), and non-first union options are the allocation exceptions. Use `ZodString.IsValidSpan` for an allocation-free span check. See [Guarantees and Limitations](Guarantees-and-Limitations.md).

## 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:
Expand All @@ -119,9 +122,9 @@ The char-scan is ~20% faster than the previous regex on the valid path, is versi
## Optimizations that make it fast

1. **Struct-based rules** — every rule is a `readonly record struct` implementing `IValidationRule<T>`, so there is no per-validation object allocation.
2. **Zero-allocation helpers** — `Span<T>`/`ReadOnlySpan<T>` string validation (`ValidateSpan`) and `ArrayPool<T>`-backed helpers.
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](Compiled-Validators-and-Caching.md)).
4. **Source generation** — `[ZodSchema]` emits direct property access and typed equality checks with no reflection (see [Source Generator](Source-Generator.md)).
5. **Fluent composition** — schemas are immutable and shareable, so `SchemaCache` avoids repeated construction (see [Compiled Validators and Caching](Compiled-Validators-and-Caching.md)).
5. **Cacheable schema instances** — a fully built schema is safe to cache and share, so `SchemaCache` avoids repeated construction (see [Compiled Validators and Caching](Compiled-Validators-and-Caching.md)). Building is not immutable: the fluent rule methods mutate the receiver. See [Custom Rules](Custom-Rules.md) for extending rules.

The only allocations on a successful validation are the string transforms (`ToLower`/`ToUpper`/`Trim` produce new strings) and the union non-first-option paths noted above.
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.
10 changes: 7 additions & 3 deletions docs/wiki/Source-Generator-DataAnnotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Size attributes generate direct `Length` or `Count` access when possible:
Structured size failures expose the same metadata as the runtime API:

- `Code`: `too_small` or `too_big`.
- `Origin`: `string` for strings, `array` for arrays and collections.
- `Origin`: `string` for strings, `array` for arrays, `collection` for countable/`IEnumerable` collections.
- `Minimum` / `Maximum`: the inclusive bound.
- `Inclusive`: `true`.
- `Path`: the property path.
Expand All @@ -60,7 +60,7 @@ var result = BasketSchema.Validate(new Basket { Items = ["apple"] });
```

> [!NOTE]
> Today the generator reports `Origin = "array"` for both arrays and collections; there is no `"collection"` origin in generated code.
> The generator reports `Origin = "array"` for arrays and `Origin = "collection"` for countable/`IEnumerable` collections.

## Range

Expand All @@ -84,4 +84,8 @@ Misuse is reported at compile time rather than silently ignored:
- String-only attributes (`[RegularExpression]`, `[EmailAddress]`, `[Url]`, `[Phone]`, `[CreditCard]`, `[Base64String]`) on non-string targets, `[AllowedValues]`/`[DeniedValues]` on unsupported types, or `[Range]` on unsupported types → ZODSGEN006.
- `[Compare]` referencing an unknown property → ZODSGEN020.

See [Source Generator Diagnostics](Source-Generator-Diagnostics.md) for the full list.
See [Source Generator Diagnostics](Source-Generator-Diagnostics.md) for the full list.

## Custom attributes

The same pipeline honours custom rules exposed as validation attributes. Mark the attribute with `[ZodRule(typeof(MyRule))]` (or mark the rule itself with `[ZodRule]` to have the attribute generated), and properties annotated with it are validated through the rule. See [Custom Rules](Custom-Rules.md).
4 changes: 4 additions & 0 deletions docs/wiki/Source-Generator-Diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ The `[ZodSchema]` generator ships an analyzer (category `ZodSharp.SourceGenerato
| ZODSGEN027 | `IValidateOptions` requested but `Microsoft.Extensions.Options` reference is missing |
| ZODSGEN028 | `IValidateOptions` requested on a struct (requires a class) |
| ZODSGEN029 | A model declares both a synchronous refinement method and an async custom validation method (only one is allowed) |
| ZODSGEN030 | A custom rule mapped through `[ZodRule(typeof(...))]` does not implement `IValidationRule<T>` for the property type (including an unbound generic rule that cannot be closed with it) |
| ZODSGEN031 | A custom rule constructor parameter could not be mapped from the attribute (`[ZodRule]`) |
| ZODSGEN032 | A validation attribute could not be generated for a rule marked `[ZodRule]` |
| ZODSGEN033 | (warning) A rule-mapped attribute is applied to a type that gets no generated schema (no `[ZodSchema]` and not reachable as a complex property), so the rule never runs |

## Suppressing

Expand Down
Loading
Loading