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.
- 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.
Install the core Purview.ZodSharp package, plus the optional JSON integration packages you need:
Install-Package Purview.ZodSharp
Install-Package Purview.ZodSharp.SystemTextJson
Install-Package Purview.ZodSharp.NewtonsoftJson
Install-Package Purview.ZodSharp.AspNetCoredotnet add package Purview.ZodSharp
dotnet add package Purview.ZodSharp.SystemTextJson
dotnet add package Purview.ZodSharp.NewtonsoftJson
dotnet add package Purview.ZodSharp.AspNetCore<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.
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/TimeOnlybounds in[Range]validation, genericEnum.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 NETSTANDARDbranches 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.
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);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...
}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();var optionalSchema = Z.Optional(Z.String());
var result1 = optionalSchema.Validate(null); // Success
var result2 = optionalSchema.Validate("value"); // Successtry
{
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}");
}
}ZodSharp is designed for maximum performance with zero-allocation validation and struct-based rules. Here's what makes it fast:
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
ZodSharp implements several optimizations for maximum performance:
- Validation rules implemented as
structto avoid allocations - Use of
Span<T>andReadOnlySpan<T>when appropriate - Object pooling for reusable schemas
All validation rules are structs:
public readonly struct MinLengthRule : IValidationRule<string>
{
// Zero allocation validation
}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 validationFluent API that allows schema composition:
var schema = Z.String()
.Min(3)
.Max(50)
.Email()
.Describe("User email address");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.
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.
Transform values during validation:
var schema = Z.String().Transform(s => s.ToUpper());
var result = schema.Validate("hello"); // "HELLO"Add custom validations:
var schema = Z.Number().Refine(n => n % 2 == 0, "Must be even");
var result = schema.Validate(4); // SuccessCreate recursive and circular schemas:
var categorySchema = Z.Lazy<Dictionary<string, object?>>(() =>
Z.Object()
.Field("name", Z.String())
.Field("subcategories", Z.Array(categorySchema))
.Build()
);Optimized unions with discriminator:
var union = Z.DiscriminatedUnion("type")
.Option("user", userSchema)
.Option("admin", adminSchema)
.Build();Default values when input is null:
var schema = Z.String().Default("unknown");
var result = schema.Validate(null); // "unknown"ZodSharp ships separate integration packages for the two major .NET JSON libraries.
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();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();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.
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);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);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 for maximum performance:
using ZodSharp.Expressions;
var compiled = CompiledValidator.Compile(schema);
var result = compiled(value); // Ultra-fast validationIntelligent schema caching:
using ZodSharp.Core;
var schema = SchemaCache.GetOrCreate("user", () =>
Z.Object().Field("name", Z.String()).Build()
);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
[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 parseddecimal,DateTime,DateOnly, andTimeOnlybounds[RegularExpression]on strings, with DataAnnotations-compatiblenulland 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_smallortoo_bigOrigin:string,array, orcollectionMinimum/MaximumInclusivePath
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 == 2Install 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"]
});
}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);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.4inDirectory.Packages.propsis a minimum version requirement, not an exact pin, so the resolved graph can drift as newer packages are published.
MIT License - see the LICENSE file for details.
Contributions are welcome! Please open an issue or pull request.
- guinhx/ZodSharp — the original project this repository was forked from.
- Zod