Source Generator
Source Generator
Section titled “Source Generator”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 failureGenerated types
Section titled “Generated types”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");Attribute options
Section titled “Attribute options”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. |
Custom async validation
Section titled “Custom async validation”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
CustomValidationAsyncunless overridden withCustomValidationMethodNameon the[ZodSchema]attribute. - A method declared on the model type must be
static; a method on the generated{TypeName}SchemaValidatorpartial may be an instance method. - The generated
ValidateAsyncruns the synchronousValidate, then awaits the custom method, and merges the error sets.
Refinement hook (OnZodValidate)
Section titled “Refinement hook (OnZodValidate)”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), whereTis the model type (ZODSGEN035). The parameter is a plain by-valueRefineCtx<T>. - The hook runs for every entry point into the generated schema —
Validate,Parse, theIZodSchemaValidatoradapter,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
Validateonly builds aRefineCtx<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.
IValidateOptions support
Section titled “IValidateOptions support”Generated options validators are enabled by:
GenerateIValidateOptions = trueon the attribute, or- auto-detection:
GenerateIValidateOptionsunset, target is not a value type, and the type name ends with a configured suffix (defaultOptionsorSettings), or - 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 |
Collection null items
Section titled “Collection null items”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).
Dictionaries
Section titled “Dictionaries”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.
DateTimeOffset wire form
Section titled “DateTimeOffset wire form”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:
<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.
Automatic enum validation
Section titled “Automatic enum validation”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.
What is validated
Section titled “What is validated”-
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
nullwithinvalid_type. -
A
Purview.ValueObjectsscalar 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 aNullable<T>— see Value Objects Integration. -
A rule marked with the parameterless
[ZodRule]generates a matching validation attribute. Every built-in rule ships its attribute insidePurview.ZodSharp(in theZodSharp.Rulesnamespace —[Email],[E164],[Regex],[NonSentinel],[MinValue],[Even], …; names that collide withSystem.ComponentModel.DataAnnotationsuse aZodsuffix 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.