Skip to content
Purview Dev logo

Source Generator

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

Mark a class, struct, or record with [ZodSchema] and the generator emits a static, zero-allocation validator at compile time. The [ZodSchema] attribute is generated into the ZodSharp namespace by the generator itself (assembly Purview.ZodSharp.SourceGenerators), so no extra package is needed beyond Purview.ZodSharp.

using System.ComponentModel.DataAnnotations;
using ZodSharp;
[ZodSchema]
public class User
{
[Required]
[StringLength(50, MinimumLength = 3)]
public string Name { get; set; } = string.Empty;
[Range(0, 120)]
public int Age { get; set; }
[EmailAddress]
public string? Email { get; set; }
}
var result = UserSchema.Validate(user);
var validated = UserSchema.Parse(user); // throws ZodException on failure

For a [ZodSchema] target type {TypeName}, the generator emits:

Artifact Shape
{TypeName}Schema static partial class — the validator; access mirrors the target (public/internal/private for private nested types); the name is overridable with SchemaName; contains Validate, Parse, BuildJsonSchema, and (when composition is enabled) ApplyAnd, ApplyOr, ApplyRefine
{TypeName}SchemaValidator partial class {TypeName}SchemaValidator : IZodSchemaValidator<{TypeName}>, IJsonSchemaProvider — DI-friendly adapter with Validate / ValidateAsync / BuildJsonSchema; emitted only for the primary schema, and named {SchemaName}Validator when SchemaName is set
{TypeName}Validator sealed partial class {TypeName}Validator : IValidateOptions<{TypeName}> — emitted only when IValidateOptions support is enabled (and the target is a class)
[assembly: ZodSchemaGenerated(typeof({TypeName}))] registration marker consumed by IZodSchemaFactory assembly scanning; emitted only for primary, non-nested schemas

BuildJsonSchema returns a ZodSharp.JsonSchema.JsonSchemaDefinition built from the same metadata as Validate, so a generated [ZodSchema] schema can be exported through Z.ToJsonSchema without rebuilding its rules by hand. See JSON Schema Export.

// Value-first composition methods (EnableComposition, default true):
var adult = UserSchema.ApplyRefine(user, u => u.Age >= 18, "Must be adult");
var both = UserSchema.ApplyAnd(user, u => u.Name.Length > 5, "Name too short");
var either = UserSchema.ApplyOr(user, u => u.Age < 18, "Must be an adult or a minor with consent");

All options are optional.

Property Default Purpose
SchemaName null Overrides the generated schema class name (default {TypeName}Schema). The DI adapter becomes {SchemaName}Validator.
GenerateValidateMethod true Set to false to omit Validate (and the members that depend on it).
GenerateParseMethod true Set to false to omit Parse. Parse requires Validate, so it is also omitted when GenerateValidateMethod = false.
EnableComposition true Emits ApplyAnd, ApplyOr, ApplyRefine value-first composition methods.
CustomValidationMethodName null Name of an async custom validation method; default lookup name CustomValidationAsync. Mutually exclusive with the synchronous OnZodValidate refinement hook.
GenerateIValidateOptions false Force IValidateOptions<T> generation.
SuppressIValidateOptions false Opt out even when auto-detection would enable it.
ValidateEnumValues true Set to false to skip the automatic enum validation for the type’s enum properties.
GenerateJsonSchema true Set to false to omit the JSON Schema export members (BuildJsonSchema and the IJsonSchemaProvider on the adapter), keeping the type out of the cross-platform contract surface. A type referenced by an exported type is reported as ZODSGEN045.
AllowNulls unset Whether this type’s collection properties may contain null elements (or null dictionary values). Unset applies the assembly default; true/false overrides it for this type. See Collection null items.

Declare a partial {TypeName}SchemaValidator (or a static method on the model type):

public partial class UserSchemaValidator
{
public async ValueTask<ValidationResult<User>> CustomValidationAsync(User value, CancellationToken ct)
{
await Task.Delay(1, ct);
return ValidationResult<User>.Success(value);
}
}

Requirements:

  • Signature ValueTask<ValidationResult<T>> Name(T value, CancellationToken ct).
  • Default lookup name CustomValidationAsync unless overridden with CustomValidationMethodName on the [ZodSchema] attribute.
  • A method declared on the model type must be static; a method on the generated {TypeName}SchemaValidator partial may be an instance method.
  • The generated ValidateAsync runs the synchronous Validate, then awaits the custom method, and merges the error sets.

Refinement rules are written as a generator-declared partial method on the model. The generator emits the declaration, so the IDE offers the implementation with the correct signature and no name is resolved by convention:

[ZodSchema]
public partial class Order
{
public decimal Total { get; set; }
partial void OnZodValidate(RefineCtx<Order> context)
{
if (context.Value.Total < 0)
context.AddIssue("invalid_range", "Total cannot be negative", [nameof(Total)]);
}
}

Requirements:

  • The target type and every containing type must be declared partial (ZODSGEN034). This is the only type-shape requirement the hook adds.
  • The signature must be partial void OnZodValidate(RefineCtx<T> context), where T is the model type (ZODSGEN035). The parameter is a plain by-value RefineCtx<T>.
  • The hook runs for every entry point into the generated schema — Validate, Parse, the IZodSchemaValidator adapter, IValidateOptions, and any factory that validates through the schema — so a rule written here behaves exactly like an attribute rule.
  • A type that does not implement the hook allocates nothing: the generated Validate only builds a RefineCtx<T> and calls the hook when a body exists.
  • Issues are reported through context.AddIssue(code, message, path), which is merged with the attribute-rule issues into one result.
  • Refinements state is reported by ZODSGEN036 if a member still uses the retired IEnumerable<ValidationError> Validate() contract, which the generator no longer binds.

Generated options validators are enabled by:

  1. GenerateIValidateOptions = true on the attribute, or
  2. auto-detection: GenerateIValidateOptions unset, target is not a value type, and the type name ends with a configured suffix (default Options or Settings), or
  3. MSBuild override.

MSBuild switches:

Property Default Behaviour
DisableZodSharpSourceGenerator unset disables the generator entirely when truthy
ZodSharpAutoGenerateOptionsValidators true auto-detect IValidateOptions (only explicit false disables)
ZodSharpAutoGenerateOptionsValidatorSuffixes Options;Settings semicolon/comma-separated suffix list
ZodSharpGenerateJsonSchemas true assembly-wide JSON Schema export switch; explicit false omits BuildJsonSchema and IJsonSchemaProvider for every type
ZodSharpEnumSerialization Number String makes enums export as their member names, matching a global JsonStringEnumConverter
ZodSharpEnumNamingPolicy Unchanged CamelCase applies JsonNamingPolicy.CamelCase to string-serialized enum names
ZodSharpAllowNulls false assembly-wide default for whether collection properties may contain null elements; explicit true accepts them
ZodSharpDateTimeOffsetSerialization PreserveOffset Utc narrows [NonSentinel] DateTimeOffset to a zero-offset wire contract; the matching converter must also reject non-UTC input. Both modes export an exact sentinel pattern

A collection property rejects null elements by default: the generated validator reports invalid_type at the element’s own path (["Items", 0]), matching Zod’s per-element issue path. The behaviour is controlled by the same setting at several levels, and the most specific statement wins:

Level Declaration
Property (or positional record parameter) [CollectionBehavior(AllowNulls = true)] / [CollectionBehavior(AllowNulls = false)]
Element type an explicitly nullable element (T?, for example List<Child?> or Child?[]) allows nulls
Containing type [ZodSchema(AllowNulls = true)] / [ZodSchema(AllowNulls = false)]
Assembly [assembly: ZodSharpSchemaDefaults(AllowNulls = ...)], then the ZodSharpAllowNulls MSBuild property

Precedence, highest first: the property attribute, the element’s T? annotation, the containing type’s [ZodSchema] option, the assembly attribute, the ZodSharpAllowNulls MSBuild property, then the built-in default (reject).

A property whose type implements IDictionary<TKey, TValue> or IReadOnlyDictionary<TKey, TValue> is a map, not a sequence: Dictionary, SortedDictionary, ConcurrentDictionary, ImmutableDictionary and custom implementations all qualify. The generated validator iterates the entries, validates each value against the value type’s nested schema (discovered recursively, with or without its own [ZodSchema]), and reports a failure at the entry’s key — ["Variants", "blue"] — matching Zod’s z.record issue path. Keys are used only in error paths; they are not validated.

The null-items setting above applies to a map’s values: Dictionary<string, Child> rejects a null value by default, while Dictionary<string, Child?> or [CollectionBehavior(AllowNulls = true)] accepts one. Dictionary<string, int> has no null case at all, so [CollectionBehavior] on it is reported as ZODSGEN046. [Length]/[MinLength]/[MaxLength] count entries, exactly as they count elements for a sequence.

using ZodSharp;
[assembly: ZodSharpSchemaDefaults(AllowNulls = false)]
[ZodSchema]
public class RepositoryInventoryResponse
{
// Rejected by default: a null element reports invalid_type at ["Items", index].
public RepositoryInventoryItem[] Items { get; set; } = [];
// Explicit opt-in for one property.
[CollectionBehavior(AllowNulls = true)]
public RepositoryInventoryItem[] OptionalItems { get; set; } = [];
// A map: the setting governs null values, and failures are reported at ["Variants", key].
public Dictionary<string, VariantConfiguration> Variants { get; set; } = [];
}

A collection whose elements may be null exports its items schema with a null branch, mirroring how a nullable property is exported. A dictionary exports { "type": "object", "additionalProperties": … } — see JSON Schema Export.

The JSON Schema generated for [NonSentinel] DateTimeOffset depends on the wire form assumed for the value. By default offsets are preserved and the pattern is offset-aware: it rejects every offset-equivalent representation of the instant-based bounds, so no unsupported marker is emitted. An optional narrower UTC contract is available:

Directory.Build.props
<ZodSharpDateTimeOffsetSerialization>Utc</ZodSharpDateTimeOffsetSerialization>
using ZodSharp;
[assembly: ZodSharpSchemaDefaults(
DateTimeOffsetSerialization = DateTimeOffsetSerialization.Utc)]
[ZodSchema]
[DateTimeOffsetBehavior(DateTimeOffsetSerialization.Utc)]
public sealed class Observation
{
public DateTimeOffset ObservedAt { get; set; }
// Override the project/type default for one property.
[DateTimeOffsetBehavior(DateTimeOffsetSerialization.PreserveOffset)]
[JsonConverter(typeof(PreserveOffsetDateTimeOffsetConverter))]
public DateTimeOffset LegacySeenAt { get; set; }
}

Precedence, highest first: [DateTimeOffsetBehavior] on the property (or positional record parameter), the containing type’s attribute, the assembly attribute, the ZodSharpDateTimeOffsetSerialization MSBuild property, then PreserveOffset. The attribute shapes the schema only; pair Utc with the integration package’s UtcDateTimeOffsetConverter (and PreserveOffset under a global UTC converter with PreserveOffsetDateTimeOffsetConverter). Both modes export an exact sentinel pattern; Utc additionally narrows the accepted wire form to a zero offset. See JSON Schema Export.

Every non-flags enum property is validated automatically: the generated validator rejects a value that is not a defined member of the enum type. The check is emitted as an EnumRule<TEnum> (see Validation Rules Reference) and reports invalid_enum_value.

A member that is defined but never a valid value can be excluded globally by marking it [ZodIgnore], and excluded for a single property with [DeniedValues]:

using System.ComponentModel.DataAnnotations;
using ZodSharp;
public enum ExampleEnum
{
[ZodIgnore]
Unspecified,
AValidValue,
AnotherValidValue,
}
[ZodSchema]
public class Model
{
// Rejects anything that is not AValidValue or AnotherValidValue.
public ExampleEnum Status { get; set; }
// Also rejects AnotherValidValue for this property only.
[DeniedValues(ExampleEnum.AnotherValidValue)]
public ExampleEnum SecondaryStatus { get; set; }
}

The automatic validation is skipped when:

  • the enum is declared [Flags] — a combination is a valid value without being a defined member;
  • the property declares an explicit [AllowedValues] allow-list, which governs the property instead;
  • the schema opts out with [ZodSchema(ValidateEnumValues = false)].

A nullable enum property is validated only when it is not null.

  • Properties must be public, non-static, non-indexer.

  • Validation is emitted for a property when it carries any DataAnnotations attribute, its type is a source-defined complex type with a nested schema, or its type is an enum (see Automatic enum validation).

  • Classes, structs, and records are supported; structs do not receive IValidateOptions (ZODSGEN028 if requested).

  • Nested complex types are discovered recursively and get their own generated {TypeName}Schema, even when the nested type does not itself carry [ZodSchema].

  • Nullable properties are null-guarded before value-set/type validation; a nullable target rejects null with invalid_type.

  • A Purview.ValueObjects scalar marked with [Scalar] can carry [ZodSchema] on the same type; the generated validator validates the scalar as a unit and reports an empty path. A rule written against the scalar’s underlying value is adapted automatically, or resolved against the unwrapped value and null-guarded when the underlying value is a Nullable<T> — see Value Objects Integration.

  • A rule marked with the parameterless [ZodRule] generates a matching validation attribute. Every built-in rule ships its attribute inside Purview.ZodSharp (in the ZodSharp.Rules namespace — [Email], [E164], [Regex], [NonSentinel], [MinValue], [Even], …; names that collide with System.ComponentModel.DataAnnotations use a Zod suffix such as [MinLengthZod]), so they can annotate a member or a scalar value object directly — see Built-in attributes.

See Source Generator DataAnnotations for the attribute coverage and structured issue shape, Custom Rules for extending validation with your own rules and attributes, and Source Generator Diagnostics for the ZODSGEN* diagnostics.