Skip to content

Composition and Transforms

Preview ZodSharp Reviewed 2026-09-16 purview-dev/zodsharp 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>, so the composition methods below are available on every schema (guarded so they only apply when input equals output). Each method returns a new wrapping schema; the original is unchanged.

var schema = Z.String().Transform(s => s.ToUpperInvariant());
var result = schema.Validate("hello"); // "HELLO"

ZodTransform<TInput, TOutput> validates the input schema first, then runs the transform. If the transform throws, the exception is caught into a transform_error ValidationError ("Transform failed: {message}").

Transforms chain:

var schema = Z.String().Transform(s => s.Trim()).Transform(s => s.ToUpperInvariant());

ZodString ships convenience transforms: .ToLower(), .ToUpper(), and .Trim().

ZodRefinement<T> runs the base schema first, then a predicate. A failing predicate produces code refinement_failed (custom message or "Custom validation failed").

var even = Z.Number().Refine(n => n % 2 == 0, "Must be even");

ZodSuperRefinement<T> takes an Action<RefineCtx<T>> and can emit multiple, path-located issues.

var password = Z.String().SuperRefine(ctx =>
{
if (!ctx.Value.Any(char.IsUpper))
ctx.AddIssue("Must contain an uppercase letter", new[] { "uppercase" });
if (ctx.Value.Length < 8)
ctx.AddIssue("too_short", "Must be at least 8 characters", new[] { "length" });
});

RefineCtx<T> exposes:

  • T Value — the value being refined.
  • ImmutableArray<string> Path — the base path.
  • AddIssue(string code, string message, string[]? path) — appends to the base path; throws ArgumentException for null/whitespace code or message.
  • AddIssue(string message, string[]? path) — shorthand with code refinement_failed.
  • Issues / HasIssues.

ZodPipe<TSourceOutput, TTargetOutput> runs the source schema, then validates its output against a target schema.

var schema = Z.String().Pipe(Z.String().Min(10));

ZodCatch<T> swallows inner failures and returns a fallback as a successful result. The fallback can be a constant or a factory that receives the input and the errors.

var withFallback = Z.String().Catch("n/a");
var computed = Z.Number().Catch((value, errors) => 0);

ZodDefault<T> substitutes a value when input is null and reports IsOptional = true and ProvidesValueOnMissing = true. The default is not re-validated.

var schema = Z.String().Default("unknown");
var result = schema.Validate(null); // "unknown"

ZodPrefault<T> substitutes a value when the input equals default(T), then still validates the substituted value through the inner schema.

var schema = Z.Number().Prefault(1);
  • .And(other)ZodIntersection<T> — both must succeed (see Unions and Discriminated Unions).
  • .Or<TOther>(other)ZodTypedUnion<T, TOther> — either may succeed.
var schema = Z.String()
.Min(3)
.Transform(s => s.Trim())
.Refine(s => s.StartsWith("PUR-", StringComparison.Ordinal), "Must start with PUR-")
.Default("PUR-UNKNOWN");
Wrapper Trigger Re-validates substituted value
Default(value) null input No
Prefault(value) input equals default(T) Yes
Catch(value|factory) inner schema fails No (returns fallback as success)
Refine(predicate) predicate returns false
SuperRefine(action) issues added to context