Skip to content

Repository files navigation

ZodSharp

NuGet version

ZodSharp is a high-performance schema validation library for C#, ported from TypeScript Zod. It features zero-allocation validation, struct-based rules, fluent API, and source generator support for maximum performance.

This project is a fork of guinhx/ZodSharp, maintained at github.com/purview-dev/ZodSharp under the Purview.* package IDs.

Release

Key Features

  • Zero-allocation validation - Minimizes allocations using structs and Span<T>
  • Struct-based rules - Validation rules implemented as structs to avoid GC
  • Fluent API - Fluent and extensible API similar to original Zod
  • Type-safe - Strong typing with advanced C# generics
  • High performance - Sub-microsecond validation times, 10x faster than reflection-based validation
  • Cross-platform - Works on .NET 8.0, .NET 9.0 and .NET 10.0
  • Source Generators - Compile-time validator generation with [ZodSchema] attribute
  • DataAnnotations Support - Automatic validation from [Required], [StringLength], [Length], [MinLength], [MaxLength], [Range], [RegularExpression], [AllowedValues], [DeniedValues], [EmailAddress], etc.

Installation

Install the core Purview.ZodSharp package, plus the optional JSON integration packages you need:

NuGet Package Manager

Install-Package Purview.ZodSharp
Install-Package Purview.ZodSharp.SystemTextJson
Install-Package Purview.ZodSharp.NewtonsoftJson
Install-Package Purview.ZodSharp.AspNetCore

.NET CLI

dotnet add package Purview.ZodSharp
dotnet add package Purview.ZodSharp.SystemTextJson
dotnet add package Purview.ZodSharp.NewtonsoftJson
dotnet add package Purview.ZodSharp.AspNetCore

PackageReference

<PackageReference Include="Purview.ZodSharp" Version="2.0.0" />
<PackageReference Include="Purview.ZodSharp.SystemTextJson" Version="2.0.0" />
<PackageReference Include="Purview.ZodSharp.NewtonsoftJson" Version="2.0.0" />
<PackageReference Include="Purview.ZodSharp.AspNetCore" Version="2.0.0" />
  • Purview.ZodSharp — Core validation library and source generator ([ZodSchema]).
  • Purview.ZodSharp.SystemTextJson — System.Text.Json integration and JSON Schema import.
  • Purview.ZodSharp.NewtonsoftJson — Newtonsoft.Json integration and JSON Schema import.
  • Purview.ZodSharp.AspNetCore — ASP.NET Core ProblemDetails integration.

Breaking Changes

Target Frameworks: .NET Standard 2.1 → .NET 8, .NET 9, .NET 10

Starting with v2.0.0, all ZodSharp library packages (Purview.ZodSharp, Purview.ZodSharp.SystemTextJson, Purview.ZodSharp.NewtonsoftJson, Purview.ZodSharp.AspNetCore) no longer target netstandard2.1. They now multi-target net8.0, net9.0 and net10.0. This is a breaking change for consumers running on older runtimes.

Why the packages moved:

  • Zero-allocation performance requires modern .NET. The library's span-based validation, struct-based rules, and reflection-free hot paths rely on modern BCL APIs — DateOnly/TimeOnly bounds in [Range] validation, generic Enum.IsDefined<T>, ArgumentNullException.ThrowIfNull, and more — none of which exist on .NET Standard 2.1.
  • .NET Standard 2.1 has no dedicated runtime. It is implemented only by .NET Core 3.0+ and is not supported by .NET Framework, so targeting it added maintenance cost without meaningful reach.
  • Removes conditional-compilation burden. Multi-targeting forced #if NETSTANDARD branches and API workarounds throughout the codebase. Dropping the target lets the library use modern APIs unconditionally.
  • Ecosystem direction. New libraries are encouraged to multi-target concrete, in-support runtimes instead of .NET Standard 2.1.

The source generator (Purview.ZodSharp.SourceGenerators) ships inside the Purview.ZodSharp package and is unaffected: it remains on netstandard2.0 because Roslyn generators must run inside any compiler host, including .NET Framework-based tooling.

Migrating: retarget your application to .NET 8 (LTS) or later. No API changes are required.

Usage Examples

Basic Validation

using ZodSharp;
using ZodSharp.Core;

// String validation
var nameSchema = Z.String().Min(3).Max(50);
var result = nameSchema.Validate("John");
if (result.IsSuccess)
{
    Console.WriteLine($"Valid name: {result.Value}");
}

// Number validation
var ageSchema = Z.Number().Min(0).Max(120).Int();
var ageResult = ageSchema.Validate(25.0);

// Additional number validations
var positiveSchema = Z.Number().Positive();
var negativeSchema = Z.Number().Negative();
var multipleOfSchema = Z.Number().MultipleOf(10); // Must be multiple of 10
var finiteSchema = Z.Number().Finite(); // Not Infinity
var safeSchema = Z.Number().Safe(); // Safe integer

// Email validation
var emailSchema = Z.String().Email();
var emailResult = emailSchema.Validate("user@example.com");

// URL validation
var urlSchema = Z.String().Url();
var urlResult = urlSchema.Validate("https://example.com");

// UUID validation
var uuidSchema = Z.String().Uuid();
var uuidResult = uuidSchema.Validate("550e8400-e29b-41d4-a716-446655440000");

// String transformations
var trimmedSchema = Z.String().Trim();
var upperSchema = Z.String().ToUpper();
var lowerSchema = Z.String().ToLower();

// String prefixes and suffixes
var prefixSchema = Z.String().StartsWith("https://");
var suffixSchema = Z.String().EndsWith(".com");

// Exact length
var exactLengthSchema = Z.String().Length(10);

Object Validation

var userSchema = Z.Object()
    .Field("name", Z.String().Min(1))
    .Field("age", Z.Number().Min(0).Max(120))
    .Field("email", Z.String().Email())
    .Build();

var userData = new Dictionary<string, object?>
{
    { "name", "John Doe" },
    { "age", 30.0 },
    { "email", "john@example.com" }
};

var result = userSchema.Validate(userData);
if (result.IsSuccess)
{
    var validatedUser = result.Value;
    // Use validatedUser...
}

Array Validation

var numbersSchema = Z.Array(Z.Number()).Min(1).Max(10);
var result = numbersSchema.Validate(new[] { 1.0, 2.0, 3.0 });

// Exact length
var exactLengthSchema = Z.Array(Z.String()).Length(5);

// Non-empty array
var nonEmptySchema = Z.Array(Z.String()).NonEmpty();

Optional Fields

var optionalSchema = Z.Optional(Z.String());
var result1 = optionalSchema.Validate(null); // Success
var result2 = optionalSchema.Validate("value"); // Success

Error Handling

try
{
    var value = nameSchema.Parse("AB"); // Too short - throws
}
catch (ZodException ex)
{
    Console.WriteLine($"Validation failed: {ex.Message}");
    foreach (var error in ex.Errors)
    {
        Console.WriteLine($"  - {string.Join(".", error.Path)}: {error.Message}");
    }
}

// Or use SafeParse for non-throwing validation
var result = nameSchema.SafeParse("AB");
if (!result.IsSuccess)
{
    foreach (var error in result.Errors)
    {
        Console.WriteLine($"Error: {error.Message}");
    }
}

Performance

ZodSharp is designed for maximum performance with zero-allocation validation and struct-based rules. Here's what makes it fast:

Performance Characteristics

Typical validation times (measured on .NET 10.0, Release mode):

  • Simple string validation: ~50-100 ns per validation
  • Number validation: ~30-80 ns per validation
  • Small arrays (10 items): ~500-800 ns per validation
  • Medium objects (6 fields): ~1-2 μs per validation
  • Complex objects (13 fields with nesting): ~3-5 μs per validation

Memory efficiency:

  • Zero allocations for simple validations (strings, numbers, booleans)
  • Minimal allocations for arrays and objects (only for error collections)
  • Struct-based rules avoid GC pressure
  • No reflection overhead in hot paths

Performance Optimizations

ZodSharp implements several optimizations for maximum performance:

1. Zero-allocation Validation

  • Validation rules implemented as struct to avoid allocations
  • Use of Span<T> and ReadOnlySpan<T> when appropriate
  • Object pooling for reusable schemas

2. Struct-based Rules

All validation rules are structs:

public readonly struct MinLengthRule : IValidationRule<string>
{
    // Zero allocation validation
}

3. Compiled Validators

Use expression trees to compile validators at runtime for maximum speed:

using ZodSharp.Expressions;

var compiled = CompiledValidator.Compile(schema);
var result = compiled(value); // Ultra-fast validation

4. Fluent API

Fluent API that allows schema composition:

var schema = Z.String()
    .Min(3)
    .Max(50)
    .Email()
    .Describe("User email address");

Performance Benchmarks

We maintain comprehensive performance tests in src/tests/ZodSharp.Benchmarks. Run them yourself:

# Run all performance benchmarks
dotnet run --project src/tests/ZodSharp.Benchmarks/ZodSharp.Benchmarks.csproj -c Release

# Run specific test suites
dotnet run --project src/tests/ZodSharp.Benchmarks/ZodSharp.Benchmarks.csproj -c Release --filter "*MemoryPerformanceTests*"

Key performance highlights:

  • 10x faster than reflection-based validation libraries
  • Zero allocations for primitive validations
  • Sub-microsecond validation for simple types
  • Minimal GC pressure with struct-based architecture
  • Scalable performance even with complex nested schemas

See the performance README for detailed benchmark results and optimization tips.

Architecture

src/
├── ZodSharp/              # Core validation library
│   ├── Core/              # Base interfaces and classes
│   ├── Schemas/           # Schema implementations
│   ├── Rules/             # Validation rules (structs)
│   ├── JsonSchema/        # JSON Schema definition and export (ToJsonSchema)
│   └── SourceGenerators/  # Compile-time [ZodSchema] generator
├── SystemTextJson/        # System.Text.Json integration and JSON Schema import
├── NewtonsoftJson/        # Newtonsoft.Json integration and JSON Schema import
├── AspNetCore/            # ASP.NET Core ProblemDetails integration
└── Examples.CLI/          # Usage examples

The source generator itself targets netstandard2.0 so it can run in any compiler. The library packages target net8.0, net9.0 and net10.0.

Advanced Features

Transforms

Transform values during validation:

var schema = Z.String().Transform(s => s.ToUpper());
var result = schema.Validate("hello"); // "HELLO"

Refinements

Add custom validations:

var schema = Z.Number().Refine(n => n % 2 == 0, "Must be even");
var result = schema.Validate(4); // Success

Lazy Evaluation

Create recursive and circular schemas:

var categorySchema = Z.Lazy<Dictionary<string, object?>>(() => 
    Z.Object()
        .Field("name", Z.String())
        .Field("subcategories", Z.Array(categorySchema))
        .Build()
);

Discriminated Unions

Optimized unions with discriminator:

var union = Z.DiscriminatedUnion("type")
    .Option("user", userSchema)
    .Option("admin", adminSchema)
    .Build();

Default Values

Default values when input is null:

var schema = Z.String().Default("unknown");
var result = schema.Validate(null); // "unknown"

JSON Integration

ZodSharp ships separate integration packages for the two major .NET JSON libraries.

System.Text.Json (Purview.ZodSharp.SystemTextJson)

using ZodSharp.Json;

// Deserialize and validate from string
var result = schema.DeserializeAndValidate(jsonString);

// Deserialize and validate from stream (async)
var result2 = await schema.DeserializeAndValidateAsync(jsonStream);

// Create JsonConverter with validation
var converter = schema.CreateValidatingConverter();

Newtonsoft.Json (Purview.ZodSharp.NewtonsoftJson)

using ZodSharp.Json;

// Deserialize and validate from string
var result = schema.DeserializeAndValidate(jsonString);

// Deserialize and validate from stream (async)
var result2 = await schema.DeserializeAndValidateAsync(jsonStream);

// Deserialize and validate from JToken
var result3 = schema.DeserializeAndValidate(jToken);

// Create JsonConverter with validation
var converter = schema.CreateValidatingConverter();

JSON Schema Interoperability

Share schemas between TypeScript (Zod) and C# (ZodSharp) using JSON Schema. This enables infinite interoperability, allowing you to define a schema in one language and reuse it in another.

The export API (Z.ToJsonSchema) lives in the core Purview.ZodSharp package. The import API (Z.FromJsonSchema) is provided by the JSON integration package you choose — either Purview.ZodSharp.SystemTextJson or Purview.ZodSharp.NewtonsoftJson.

Export to JSON Schema (ZodSharp -> JSON Schema)

var userSchema = Z.Object()
    .Field("name", Z.String().Min(3))
    .Field("email", Z.String().Email())
    .Field("age", Z.Number().Min(0).Int())
    .Build();

// Convert to JSON Schema object
var jsonSchema = Z.ToJsonSchema<Dictionary<string, object?>>(userSchema, new ToJsonSchemaOptions
{
    Title = "User",
    Id = "https://example.com/schemas/user.json"
});

// Serialize with your preferred JSON library
// System.Text.Json (add Purview.ZodSharp.SystemTextJson):
using ZodSharp.JsonSchema;
var systemTextJson = System.Text.Json.JsonSerializer.Serialize(jsonSchema, JsonSchemaSerializerOptions.Default);

// Newtonsoft.Json (add Purview.ZodSharp.NewtonsoftJson):
using ZodSharp.JsonSchema;
var newtonsoftJson = JsonConvert.SerializeObject(jsonSchema, JsonSchemaSerializerOptions.Default);

Import from JSON Schema (JSON Schema -> ZodSharp)

Add either Purview.ZodSharp.SystemTextJson or Purview.ZodSharp.NewtonsoftJson to your project, then:

var jsonSchemaString = @"{
    ""type"": ""object"",
    ""properties"": {
        ""name"": { ""type"": ""string"", ""minLength"": 3 },
        ""email"": { ""type"": ""string"", ""format"": ""email"" }
    },
    ""required"": [""name"", ""email""]
}";

// Parse into ZodSharp schema
var userSchema = Z.FromJsonSchema(jsonSchemaString);

// Validate data
var result = userSchema.Validate(userData);

Cross-Platform Scenario

Frontend (TypeScript/Zod):

import { z } from "zod";

const UserSchema = z.object({
  username: z.string().min(3),
  email: z.string().email()
});

// Zod v4+ natively supports JSON Schema conversion
const jsonSchema = z.toJSONSchema(UserSchema);
// Send jsonSchema to backend...

Backend (C#/ZodSharp):

// Receive jsonSchema...
var userSchema = Z.FromJsonSchema(jsonSchemaString);
var result = userSchema.Validate(incomingData);

Compiled Validators

Compiled validators for maximum performance:

using ZodSharp.Expressions;

var compiled = CompiledValidator.Compile(schema);
var result = compiled(value); // Ultra-fast validation

Schema Caching

Intelligent schema caching:

using ZodSharp.Core;

var schema = SchemaCache.GetOrCreate("user", () => 
    Z.Object().Field("name", Z.String()).Build()
);

Source Generators

Generate zero-allocation validators at compile time:

using System.ComponentModel.DataAnnotations;
using ZodSharp.SourceGenerators;

[ZodSchema]
public class User
{
    [Required]
    [StringLength(50, MinimumLength = 3)]
    public string Name { get; set; } = string.Empty;

    [Required]
    [Range(0, 120)]
    public int Age { get; set; }

    [EmailAddress]
    public string? Email { get; set; }
}

// Auto-generated validator
var result = UserSchema.Validate(user);
var validated = UserSchema.Parse(user); // Throws on failure

// Value-first composition methods (validate a value, then run an extra predicate)
var refined = UserSchema.ApplyRefine(user, u => u.Age >= 18, "Must be adult");
var combined = 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");

Features:

  • Automatic validation from DataAnnotations attributes
  • Zero-reflection, zero-allocation validators
  • Value-first composition methods (.ApplyAnd(), .ApplyOr(), .ApplyRefine()) plus instance schema-composing composition (.Refine(), .SuperRefine(), .Pipe(), .Catch(), .Prefault(), .Default())
  • Supports classes, structs, and records

Supported DataAnnotations size validators

[Length], [StringLength], [MinLength], and [MaxLength] generate direct Length or Count access when possible:

  • string -> .Length
  • arrays, including rectangular arrays -> .Length
  • jagged arrays -> outer-array .Length
  • countable collections -> .Count
  • IEnumerable / IEnumerable<T> -> a single counted pass with a non-enumerating fast path

[Length] follows DataAnnotations null semantics: null is valid unless [Required] is also present.

Other supported DataAnnotations validators:

  • [Range] on numeric types plus parsed decimal, DateTime, DateOnly, and TimeOnly bounds
  • [RegularExpression] on strings, with DataAnnotations-compatible null and empty-string behaviour
  • [AllowedValues] and [DeniedValues] using generated typed equality checks instead of runtime attribute execution
  • [EmailAddress] on strings

Structured size failures expose:

  • Code: too_small or too_big
  • Origin: string, array, or collection
  • Minimum / Maximum
  • Inclusive
  • Path

Example:

using System.ComponentModel.DataAnnotations;

[ZodSchema]
public sealed class Basket
{
    [Required]
    [Length(2, 5)]
    public List<string>? Items { get; set; }
}

var result = BasketSchema.Validate(new Basket { Items = ["apple"] });
// result.Errors[0].Code == "too_small"
// result.Errors[0].Minimum == 2

ASP.NET Core ProblemDetails

Install Purview.ZodSharp.AspNetCore to convert failed validation results into standard ASP.NET Core payloads while preserving structured issues:

using ZodSharp.AspNetCore;

var result = BasketSchema.Validate(basket);

if (!result.IsSuccess)
{
    var problem = result.ToHttpValidationProblemDetails();
    return Results.ValidationProblem(
        problem.Errors,
        extensions: new Dictionary<string, object?>
        {
            ["issues"] = problem.Extensions["issues"]
        });
}

Span Validation

Zero-allocation string validation using spans:

var schema = Z.String().Min(3).Max(50).Email();
var span = "user@example.com".AsSpan();
var result = schema.ValidateSpan(span);

Dependency Management

Package versions are declared centrally in Directory.Packages.props. No packages.lock.json files are committed; package resolution is left to NuGet at restore time.

Note: a version like 13.0.4 in Directory.Packages.props is a minimum version requirement, not an exact pin, so the resolved graph can drift as newer packages are published.

License

MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please open an issue or pull request.

Acknowledgments

About

ZodSharp is a high-performance, zero-allocation schema validation library for C#, featuring struct-based rules, a fluent API, and source-generator support inspired by Zod.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages