Skip to content

Nested & Collections

Properties whose type has [ZValidate] are automatically validated recursively:

[ZValidate]
public partial class Address
{
[ZRequired] public string Street { get; set; } = "";
[ZRequired] public string City { get; set; } = "";
[ZMatch(@"^\d{5}$")] public string? Zip { get; set; }
}
[ZValidate]
public partial class CustomerForm
{
[ZRequired] public string Name { get; set; } = "";
public Address BillingAddress { get; set; } = new();
public Address? ShippingAddress { get; set; } // null → skipped
}
var form = new CustomerForm
{
Name = "",
BillingAddress = new Address { Street = "", City = "" }
};
var result = form.Validate();
// Errors with dot-separated paths:
// "Name: Name is required."
// "BillingAddress.Street: Street is required."
// "BillingAddress.City: City is required."

Nullable properties (Address?) are skipped when null. Non-nullable properties are always validated.

Collections of [ZValidate] types are validated element-by-element with index:

[ZValidate]
public partial class LineItem
{
[ZRequired] public string Sku { get; set; } = "";
[ZRange(1, 9999)] public int Quantity { get; set; }
}
[ZValidate]
public partial class Invoice
{
[ZRequired] public string InvoiceNumber { get; set; } = "";
public List<LineItem> Lines { get; set; } = new();
}
var invoice = new Invoice
{
InvoiceNumber = "INV-001",
Lines = new()
{
new LineItem { Sku = "", Quantity = 0 },
new LineItem { Sku = "ABC", Quantity = 5 },
new LineItem { Sku = "", Quantity = -1 },
}
};
var result = invoice.Validate();
// Errors:
// "Lines[0].Sku: Sku is required."
// "Lines[0].Quantity: Quantity must be between 1 and 9999."
// "Lines[2].Sku: Sku is required."
// "Lines[2].Quantity: Quantity must be between 1 and 9999."

Context is auto-created at the root and flows through nested validators:

// Simple — context created internally:
var result = form.Validate();
// With custom data:
var result = form.Validate(new ValidationContext
{
Items = { ["tenant"] = "acme", ["userId"] = 42 }
});
PropertyTypeDescription
Parentobject?The object that triggered this nested validation
PathstringDot-separated path from root (e.g. "BillingAddress.Street")
RootObjectobject?The top-level object that started the chain
ItemsDictionary<string, object?>User-defined data bag
public sealed class ValidationResult
{
public bool IsValid { get; }
public IReadOnlyList<ValidationError> ValidationErrors { get; }
public IReadOnlyList<string> Errors { get; } // flat strings
// ASP.NET ModelState shape:
public Dictionary<string, List<string>> ToDictionary();
}
public sealed class ValidationError
{
public string Property { get; } // "BillingAddress.Street"
public string Message { get; } // "Street is required."
public string FullMessage { get; } // "BillingAddress.Street: Street is required."
}
var dict = result.ToDictionary();
// {
// "Name": ["Name is required."],
// "BillingAddress.Street": ["Street is required."],
// "BillingAddress.City": ["City is required."]
// }

Matches ASP.NET ModelStateDictionary shape — drop directly into ValidationProblem().