Skip to content
Purview Dev logo

Core Concepts

Stable ZodSharp Reviewed 2026-10-26 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

Every schema derives from ZodType<TOutput, TInput> (namespace ZodSharp.Core). Validation is a two-phase pipeline:

  1. ParseInternal — each schema overrides this hook to perform its type check and traversal (rejecting null where not allowed, coercing types, walking objects/arrays/tuples/unions, producing structured failures).
  2. Rules — on success, the accumulated IValidationRule<TOutput> structs are evaluated. A failing rule emits a ValidationError carrying the rule’s Zod-compatible code (too_small, too_big, not_multiple_of, not_finite, invalid_string, invalid_type, or invalid_value; validation_failed for custom rules without a code). Each rule exposes its reported code and message as public const string ErrorCode / MessageFormat constants, so tests can assert against the rule instead of duplicating literals (a rule that omits them is reported as ZODSGEN042 — see Custom Rules).
ValidationResult<TOutput> result = schema.Validate(value);
Member Behaviour
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; 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> is a readonly record struct in ZodSharp.Core:

  • bool IsSuccess — annotated [MemberNotNullWhen(true, nameof(Value))].
  • T? Value — the validated value; valid only when IsSuccess.
  • ImmutableArray<ValidationError> Errors — populated on failure.

Static factories: Success(value), Failure(ValidationError), Failure(ImmutableArray<ValidationError>), Failure(IEnumerable<ValidationError>), and Merge(lhs, rhs) (succeeds only if both succeed; concatenates errors).

ValidationError is a readonly record struct carrying machine-readable metadata:

  • Code — e.g. "invalid_type", "too_small", "too_big", "invalid_union", "missing_field", "unrecognized_key", "validation_failed", "refinement_failed", "transform_error".
  • Message — human-readable message.
  • Path — ImmutableArray<string>, e.g. ["user", "email"]; array indexes appear as string segments such as "0".
  • Origin — category for structured size issues ("string", "array", "collection").
  • Minimum / Maximum — inclusive bounds for size issues.
  • Inclusive — whether the bound is inclusive.
  • Parameters — IReadOnlyDictionary<string, object?>, e.g. union failures carry every option error under "errors".

Create custom issues with ValidationError.Create(code, message, path, parameters, origin, minimum, maximum, inclusive).

ZodException (namespace ZodSharp.Core) is thrown by Parse and GetValueOrThrow(). It exposes ImmutableArray<ValidationError> Errors. Its ToString() renders one line per error: "{joinedPath}: {Message} ({Code})".

try
{
var value = schema.Parse("AB");
}
catch (ZodException ex)
{
foreach (var error in ex.Errors)
Console.WriteLine($"{string.Join(".", error.Path)}: {error.Message}");
}
  • 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.
  • Custom rules can be attached with the public AddRule/Rule methods and surfaced as DataAnnotations-style attributes; see Custom Rules.
  • 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("...").

ZodType composes via wrappers rather than mutation. The base type guards that input and output types match, then returns a new schema:

  • Transform<TNew>(Func<TOutput, TNewOutput>) → ZodTransform<TOutput, TNewOutput>.
  • Refine(Func<TOutput, bool>, string? message) → ZodRefinement<TOutput>.
  • SuperRefine(Action<RefineCtx<TOutput>>) → ZodSuperRefinement<TOutput>.
  • Pipe<TTarget>(IZodSchema<TTarget, TOutput>) → ZodPipe<TOutput, TTarget>.
  • Catch(TOutput | Func<TOutput, ImmutableArray<ValidationError>, TOutput>) → ZodCatch<TOutput>.
  • Prefault(TOutput) → ZodPrefault<TOutput>.
  • Default(TOutput) → ZodDefault<TOutput>.
  • And(IZodSchema<TOutput, TOutput>) → ZodIntersection<TOutput>.
  • Or<TOther>(IZodSchema<TOther, TOther>) → ZodTypedUnion<TOutput, TOther>.

See Composition and Transforms for details and semantics.

Schemas report optionality through the internal IOptionalSchema interface:

  • IsOptional — true for ZodOptional, ZodNullable, ZodDefault, ZodPrefault.
  • ProvidesValueOnMissing — true for ZodDefault and ZodPrefault; object fields route missing values through Validate(null!) to inject the produced value.
  • IAcceptsNull.ValidateNull() is implemented by nullable/optional/default/prefault schemas and ZodNull; object-field and union wrappers route null input to it instead of failing coercion.

Object fields and union options wrap typed schemas so boxed values from a Dictionary<string, object?> can be validated:

  • null routes to IAcceptsNull.ValidateNull() where supported.
  • Exact-typed values reuse the original boxed value.
  • Numeric coercion uses IConvertible (invariant culture), so a boxed long can satisfy a Z.Number() field.
  • Non-nullable value types reject null; anything else falls through to failure.

IZodSchemaFactory is the registry that resolves validators by type. See Dependency Injection.