Zeta 0.1.10
dotnet add package Zeta --version 0.1.10
NuGet\Install-Package Zeta -Version 0.1.10
<PackageReference Include="Zeta" Version="0.1.10" />
<PackageVersion Include="Zeta" Version="0.1.10" />
<PackageReference Include="Zeta" />
paket add Zeta --version 0.1.10
#r "nuget: Zeta, 0.1.10"
#:package Zeta@0.1.10
#addin nuget:?package=Zeta&version=0.1.10
#tool nuget:?package=Zeta&version=0.1.10
Zeta
A composable, type-safe, async-first validation framework for .NET inspired by Zod.
var UserSchema = Z.Object<User>()
.Field(u => u.Email, s => s.Email())
.Field(u => u.Age, s => s.Min(18));
var result = await UserSchema.ValidateAsync(user);
if(!result.IsSuccess)
{
foreach(var error in result.Errors)
{
Console.WriteLine($"{error.Path}: {error.Message}");
}
}
Features
- Schema-first - Define validation as reusable schema objects
- Async by default - Every rule can be async, no separate sync/async paths
- Composable - Schemas are values that can be reused and combined
- Path-aware errors - Errors include location (
user.address.street,items[0]) - ASP.NET Core native - First-class support for Minimal APIs and Controllers
Installation
dotnet add package Zeta
dotnet add package Zeta.AspNetCore # For ASP.NET Core integration
Schema Types
String
Z.String()
.MinLength(3)
.MaxLength(100)
.Length(10) // Exact length
.Email()
.Uuid() // UUID/GUID format
.Url() // HTTP/HTTPS URLs
.Uri() // Any valid URI
.Alphanumeric() // Letters and numbers only
.StartsWith("prefix")
.EndsWith("suffix")
.Contains("substring")
.Regex(@"^[A-Z]")
.NotEmpty()
.Refine(s => s.StartsWith("A"), "Must start with A")
Numeric Types
Z.Int()
.Min(0)
.Max(100)
.Refine(n => n % 2 == 0, "Must be even")
Z.Double()
.Min(0.0)
.Max(100.0)
.Positive()
.Negative()
.Finite()
Z.Decimal()
.Min(0m)
.Max(1000m)
.Positive()
.Precision(2) // Max 2 decimal places
.MultipleOf(0.25m) // Must be multiple of step
Date and Time
Z.DateTime()
.Min(minDate)
.Max(maxDate)
.Past() // Must be in the past
.Future() // Must be in the future
.Between(min, max)
.Weekday() // Monday-Friday only
.Weekend() // Saturday-Sunday only
.WithinDays(7) // Within N days from now
.MinAge(18) // For birthdate validation
.MaxAge(65)
Z.DateOnly()
.Min(minDate).Max(maxDate).Past().Future()
.Between(min, max).Weekday().Weekend()
.MinAge(18).MaxAge(65)
Z.TimeOnly()
.Min(minTime).Max(maxTime).Between(min, max)
.BusinessHours() // 9 AM - 5 PM (default)
.BusinessHours(start, end) // Custom hours
.Morning() // 6 AM - 12 PM
.Afternoon() // 12 PM - 6 PM
.Evening() // 6 PM - 12 AM
Other Types
Z.Guid()
.NotEmpty() // Not Guid.Empty
.Version(4) // Specific UUID version (1, 2, 3, 4, or 5)
Z.Bool()
.IsTrue() // Must be true (e.g., terms accepted)
.IsFalse() // Must be false
Nullable (Optional Fields)
All schemas are required by default. Use .Nullable() to allow null values:
Z.String().Nullable() // Allows null strings
Z.Int().Nullable() // Allows null ints
Z.Object<Address>().Nullable() // Allows null objects
Nullable value type fields (int?, double?, etc.) are handled automatically in object schemas — null values skip validation, non-null values are validated. No need to call .Nullable():
public record User(string Name, int? Age, decimal? Balance, string? Bio);
Z.Object<User>()
.Field(u => u.Name, s => s.MinLength(2))
.Field(u => u.Age, s => s.Min(0).Max(120)) // int? — null skips validation
.Field(u => u.Balance, s => s.Positive().Precision(2)) // decimal? — null skips validation
.Field(u => u.Bio, s => s.MaxLength(500).Nullable()) // string? — call .Nullable() to allow null
For nullable reference types (string?), call .Nullable() on the schema if null should be a valid value.
Object
Define field schemas inline with builder functions:
Z.Object<User>()
.Field(u => u.Name, s => s.MinLength(2))
.Field(u => u.Email, s => s.Email().MinLength(5))
.Field(u => u.Age, s => s.Min(18).Max(100))
.Field(u => u.Price, s => s.Positive().Precision(2))
.Refine(u => u.Password != u.Email, "Password cannot be email");
// Supported for: string, int, double, decimal, bool, Guid, DateTime, DateOnly, TimeOnly
For Composability - Extract reusable schemas when needed across multiple objects:
var AddressSchema = Z.Object<Address>()
.Field(a => a.Street, s => s.MinLength(3))
.Field(a => a.ZipCode, s => s.Regex(@"^\d{5}$"));
Z.Object<User>()
.Field(u => u.Name, s => s.MinLength(2))
.Field(u => u.Address, AddressSchema); // Reuse nested schema
Collections
Validate arrays and lists with .Each() for element validation:
// Simple element validation
Z.Collection<string>()
.Each(s => s.Email()) // Validate each element
.MinLength(1) // Collection-level validation
.MaxLength(10)
Z.Collection<int>()
.Each(n => n.Min(0).Max(100))
.NotEmpty()
// In object schemas with fluent builders
Z.Object<User>()
.Field(u => u.Tags, tags => tags
.Each(s => s.MinLength(3).MaxLength(50))
.MinLength(1).MaxLength(10));
// Complex nested objects - pass pre-built schema
var orderItemSchema = Z.Object<OrderItem>()
.Field(i => i.ProductId, s => s)
.Field(i => i.Quantity, s => s.Min(1));
Z.Collection(orderItemSchema)
.MinLength(1);
// Errors include index path: "tags[0]", "items[2].quantity"
See the Collections guide for advanced patterns including context-aware validation.
Conditional Validation
Use .When() for dependent field validation:
Z.Object<Order>()
.Field(o => o.PaymentMethod, Z.String())
.Field(o => o.CardNumber, Z.String().Nullable())
.Field(o => o.BankAccount, Z.String().Nullable())
.When(
o => o.PaymentMethod == "card",
then: c => c.Require(o => o.CardNumber), // CardNumber required when paying by card
@else: c => c.Require(o => o.BankAccount) // BankAccount required otherwise
);
Use .Select() for inline schema building within conditionals:
Z.Object<User>()
.Field(u => u.Password, Z.String().Nullable())
.Field(u => u.RequiresStrongPassword, Z.Bool())
.When(
u => u.RequiresStrongPassword,
then: c => c.Select(u => u.Password, s => s.MinLength(12).MaxLength(100))
);
Result Pattern
Validation returns Result<T> instead of throwing exceptions:
var result = await schema.ValidateAsync(value);
// Pattern match
var output = result.Match(
success: value => $"Valid: {value}",
failure: errors => $"Invalid: {errors.Count} errors"
);
// Chain operations
var finalResult = await UserSchema.ValidateAsync(input)
.Then(user => SaveUserAsync(user))
.Map(saved => new UserResponse(saved.Id));
// Get value or throw
var user = result.GetOrThrow();
// Get value or default
var user = result.GetOrDefault(defaultUser);
ASP.NET Core Integration
Setup
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddZeta();
Minimal APIs
var UserSchema = Z.Object<User>()
.Field(u => u.Email, s => s.Email())
.Field(u => u.Name, s => s.MinLength(3));
app.MapPost("/users", (User user) => Results.Ok(user))
.WithValidation(UserSchema);
Validation failures return 400 Bad Request with ValidationProblemDetails:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "Validation failed",
"status": 400,
"errors": {
"email": ["Invalid email format"],
"name": ["Must be at least 3 characters"]
}
}
Controllers
Inject IZetaValidator for manual validation:
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IZetaValidator _validator;
private static readonly ISchema<User> UserSchema = Z.Object<User>()
.Field(u => u.Name, s => s.MinLength(3))
.Field(u => u.Email, s => s.Email());
public UsersController(IZetaValidator validator)
{
_validator = validator;
}
[HttpPost]
public async Task<IActionResult> Create(User user)
{
var result = await _validator.ValidateAsync(user, UserSchema);
return result.ToActionResult(valid => Ok(new
{
Message = "User created",
User = valid
}));
}
}
Result Extensions
// For Controllers - returns Ok or BadRequest
result.ToActionResult()
result.ToActionResult(v => CreatedAtAction(...))
// For Minimal APIs - returns Results.Ok or Results.ValidationProblem
result.ToResult()
result.ToResult(v => Results.Created(...))
Custom Rules
Inline Refinement
Z.String()
.Refine(
value => value.StartsWith("A"),
message: "Must start with A",
code: "starts_with_a"
)
Custom Rule Class
public sealed class StartsWithUpperRule : IValidationRule<string>
{
public ValidationError? Validate(string value, ValidationExecutionContext execution)
{
return char.IsUpper(value[0])
? null
: new ValidationError(execution.Path, "starts_upper", "Must start with uppercase");
}
}
// Usage
Z.String().Use(new StartsWithUpperRule())
Async Refinement
For async validation with context:
Z.String()
.Email()
.WithContext<UserContext>()
.RefineAsync(
async (email, ctx, ct) => !await _repo.EmailExistsAsync(email, ct),
message: "Email already taken",
code: "email_exists"
)
See the Custom Rules guide for context-aware rules and advanced patterns.
Validation Context
For async data loading before validation (e.g., checking database):
// Define context
public record UserContext(bool EmailExists);
// Factory to load context
public class UserContextFactory : IValidationContextFactory<User, UserContext>
{
private readonly IUserRepository _repo;
public UserContextFactory(IUserRepository repo) => _repo = repo;
public async Task<UserContext> CreateAsync(User input, IServiceProvider services, CancellationToken ct)
{
return new UserContext(EmailExists: await _repo.EmailExistsAsync(input.Email, ct));
}
}
// Use in schema with .WithContext()
var UserSchema = Z.Object<User>()
.Field(u => u.Name, s => s.MinLength(3))
.WithContext<User, UserContext>()
.Field(u => u.Email, s => s.Email()) // Fluent builders still work
// For context-aware validation, use pre-built schemas
.Field(u => u.Username,
Z.String()
.MinLength(3)
.WithContext<UserContext>()
.RefineAsync(async (username, ctx, ct) =>
!await ctx.Repo.UsernameExistsAsync(username, ct),
"Username already taken"));
// Register
builder.Services.AddZeta(typeof(Program).Assembly);
See the Validation Context guide for more details.
Error Model
public record ValidationError(
string Path, // "user.address.street" or "items[0]"
string Code, // "min_length", "email", "required"
string Message // "Must be at least 3 characters"
);
Standard Error Codes
| Code | Meaning |
|---|---|
required |
Value is null/missing |
min_length / max_length |
Length constraints |
length |
Not exact length |
min_value / max_value |
Value constraints |
email / uuid / url / uri |
Format validation |
alphanumeric |
Contains non-alphanumeric chars |
starts_with / ends_with / contains |
String content |
regex |
Pattern mismatch |
precision / multiple_of |
Numeric constraints |
positive / negative / finite |
Number signs |
past / future / between / within_days |
Date constraints |
weekday / weekend |
Day of week |
min_age / max_age |
Age validation |
business_hours / morning / afternoon / evening |
Time constraints |
not_empty / version |
GUID validation |
is_true / is_false |
Boolean constraints |
custom_error |
Custom refinement failed |
Benchmarks
Comparing Zeta against FluentValidation and DataAnnotations on .NET 10 (Apple M2 Pro).
| Method | Mean | Allocated |
|---|---|---|
| FluentValidation | 129.1 ns | 600 B |
| FluentValidation (Async) | 229.3 ns | 672 B |
| Zeta | 293.2 ns | 152 B |
| Zeta (Invalid) | 398.6 ns | 904 B |
| DataAnnotations | 617.8 ns | 1,848 B |
| DataAnnotations (Invalid) | 974.9 ns | 2,672 B |
| FluentValidation (Invalid) | 1,920.5 ns | 7,728 B |
| FluentValidation (Invalid Async) | 2,095.8 ns | 7,800 B |
Key findings:
- Allocates 75% less memory than FluentValidation on valid input (152 B vs 600 B)
- Allocates 8.5x less memory than FluentValidation on invalid input (904 B vs 7,728 B)
- 4.8x faster than FluentValidation when validation fails (398 ns vs 1,920 ns)
- 2.4x faster than DataAnnotations when validation fails (398 ns vs 974 ns)
Run benchmarks:
dotnet run --project benchmarks/Zeta.Benchmarks -c Release
Documentation
- Collections - Arrays, lists, and element validation patterns
- Fluent Field Builders - Inline schema definitions for object fields
- Validation Context - Async data loading and context-aware schemas
- Custom Rules - Creating reusable validation rules
- Testing - Testing strategies and TimeProvider support
- Mediator Integration - Using Zeta with MediatR pipelines
- Changelog - Release history and version changes
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Bcl.AsyncInterfaces (>= 8.0.0)
- Microsoft.Bcl.TimeProvider (>= 8.0.0)
- System.Memory (>= 4.5.5)
- System.Threading.Tasks.Extensions (>= 4.5.4)
-
net10.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Zeta:
| Package | Downloads |
|---|---|
|
Zeta.AspNetCore
ASP.NET Core integration for Zeta validation framework. Includes Minimal API filters, IZetaValidator service, and Result extensions. |
GitHub repositories
This package is not used by any popular GitHub repositories.