Skip to content
Purview Dev logo

Unions and Discriminated Unions

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

ZodUnion (namespace ZodSharp.Schemas) tries each option in order and returns the first success.

using ZodSharp;
var schema = Z.Union(Z.String(), Z.Number(), Z.Boolean());
var result = schema.Validate(42.0); // matches Z.Number()

When no option matches, a single invalid_union error is produced ("Value does not match any of the union options") whose Parameters["errors"] carries every option’s errors.

ZodTypedUnion<T1, T2> dispatches on runtime type (is T1 / is T2) and produces a Union<T1, T2> result value.

var schema = Z.Union(Z.String(), Z.Number());
var result = schema.Validate(42.0);
if (result.IsSuccess)
result.Value.Match(
str => Console.WriteLine($"string: {str}"),
num => Console.WriteLine($"number: {num}"));

The Union<...> value type (namespace ZodSharp.Unions) is the result of a typed union:

  • Create(T1) / Create(T2) (and a three-case variant) — tagged construction.
  • Implicit conversions from the case types.
  • int Tag and object Value (Value throws if uninitialized).
  • TryGetValue(out T1) / TryGetValue(out T2).
  • Match(Func<T1, TResult>, Func<T2, TResult>) and Switch(Action<T1>, Action<T2>).
  • == / !=, Equals, GetHashCode, ToString.

When targeting net11.0 or later, Z.NativeUnion<T1, T2> returns a native C# 15 union (ZodSharp.Unions.NativeUnion<T1, T2>) instead of Union<T1, T2>:

var schema = Z.NativeUnion(Z.String().Min(1), Z.Object().Build());
var result = schema.Validate("hello");
if (result.IsSuccess)
{
var length = result.Value switch
{
string s => s.Length,
Dictionary<string, object?> o => o.Count,
_ => -1,
};
}

Prefer this when both option types are reference types: the native union is allocation-free and supports exhaustive pattern matching. Value-type cases box, so keep using Z.Union<T1, T2> (the hand-rolled Union<T1, T2>) when a case is a value type, or when you need Tag/Match/Switch/TryGetValue/equality. The analyzer reports ZODSGEN041 (Info) and offers a code fix when a reference-type-only Z.Union can be switched to Z.NativeUnion.

ZodDiscriminatedUnion dispatches on a discriminator value read from the input — a dictionary key or a public instance property — resolved case-insensitively.

var union = Z.DiscriminatedUnion("type")
.Option("user", userSchema)
.Option("admin", adminSchema)
.Build();
var result = union.Validate(new Dictionary<string, object?>
{
{ "type", "user" },
{ "name", "John" }
});

Failures:

  • No discriminator present → missing_discriminator.
  • Value not among the options → invalid_discriminator listing the expected values.
  • null input → invalid_type.

The builder (ZodDiscriminatedUnionBuilder) accepts untyped IZodSchema<object, object> options via Option(string value, IZodSchema<object, object> schema) and typed options via Option<T>(string value, IZodSchema<T, T> schema), which wrap the schema for coercion and null handling.

ZodIntersection<T> (created with Z.Intersection<T>(left, right) or .And(other)) succeeds only when both schemas validate; failures merge both error sets.

var schema = Z.String().Min(3).And(Z.String().Max(10));