From 1f40b59a96a3155656c42c5b0f2ccc7a7c41439f Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Mon, 14 Sep 2026 15:52:11 +0100 Subject: [PATCH 1/4] docs: updated docs and bumped sdk --- .config/dotnet-tools.json | 7 ++ .config/lefthook.yml | 9 +- AGENTS.md | 117 +++++++++++++++++ Directory.Packages.props | 4 +- Justfile | 20 +-- LICENSE | 22 ---- README.md | 50 ++++---- assets/images/purview-logo.png | Bin 0 -> 41065 bytes global.json | 6 +- package.json | 17 ++- purview-build.json | 18 ++- src/Directory.Build.props | 13 +- src/ZodSharp.slnx | 2 +- src/src/AspNetCore/AspNetCore.csproj | 4 - src/src/AspNetCore/Sdk/README.md | 47 +++++++ .../Benchmarks}/ArrayPerformanceTests.cs | 0 .../Benchmarks}/BasicPerformanceTests.cs | 0 .../Benchmarks/Benchmarks.csproj} | 9 +- src/src/Benchmarks/Directory.Build.props | 19 +++ src/src/Benchmarks/Directory.Build.targets | 10 ++ .../Benchmarks}/HeavyPerformanceTests.cs | 0 .../Benchmarks}/MemoryPerformanceTests.cs | 0 .../Benchmarks}/ObjectPerformanceTests.cs | 0 .../Benchmarks}/Program.cs | 0 src/src/Benchmarks/README.md | 84 +++++++++++++ .../Benchmarks}/TransformPerformanceTests.cs | 0 .../Benchmarks}/UnionPerformanceTests.cs | 0 src/src/NewtonsoftJson/NewtonsoftJson.csproj | 4 - src/src/NewtonsoftJson/Sdk/README.md | 63 ++++++++++ .../AnalyzerReleases.Shipped.md | 6 +- .../Helpers/SourceGenLibrary.cs | 1 + .../Models/SchemaGenerationModel.cs | 2 + .../Models/ZodSchemaAttributeData.cs | 2 +- .../SourceGenerators/SourceGenerators.csproj | 4 +- .../ZodSchemaGenerator.BuildSchema.cs | 3 +- src/src/SystemTextJson/Sdk/README.md | 61 +++++++++ src/src/SystemTextJson/SystemTextJson.csproj | 4 - src/src/ZodSharp/Sdk/README.md | 85 +++++++++++++ src/src/ZodSharp/ZodSharp.csproj | 4 - .../AssemblyInfo.cs} | 0 .../Infra/ZodAnalyzerTestOptions.cs | 2 +- ...SchemaGeneratorTests.CompositionMethods.cs | 26 ++++ src/tests/ZodSharp.Benchmarks/README.md | 118 ------------------ .../AssemblyInfo.cs} | 0 44 files changed, 621 insertions(+), 222 deletions(-) create mode 100644 AGENTS.md delete mode 100644 LICENSE create mode 100644 assets/images/purview-logo.png create mode 100644 src/src/AspNetCore/Sdk/README.md rename src/{tests/ZodSharp.Benchmarks => src/Benchmarks}/ArrayPerformanceTests.cs (100%) rename src/{tests/ZodSharp.Benchmarks => src/Benchmarks}/BasicPerformanceTests.cs (100%) rename src/{tests/ZodSharp.Benchmarks/ZodSharp.Benchmarks.csproj => src/Benchmarks/Benchmarks.csproj} (62%) create mode 100644 src/src/Benchmarks/Directory.Build.props create mode 100644 src/src/Benchmarks/Directory.Build.targets rename src/{tests/ZodSharp.Benchmarks => src/Benchmarks}/HeavyPerformanceTests.cs (100%) rename src/{tests/ZodSharp.Benchmarks => src/Benchmarks}/MemoryPerformanceTests.cs (100%) rename src/{tests/ZodSharp.Benchmarks => src/Benchmarks}/ObjectPerformanceTests.cs (100%) rename src/{tests/ZodSharp.Benchmarks => src/Benchmarks}/Program.cs (100%) create mode 100644 src/src/Benchmarks/README.md rename src/{tests/ZodSharp.Benchmarks => src/Benchmarks}/TransformPerformanceTests.cs (100%) rename src/{tests/ZodSharp.Benchmarks => src/Benchmarks}/UnionPerformanceTests.cs (100%) create mode 100644 src/src/NewtonsoftJson/Sdk/README.md create mode 100644 src/src/SystemTextJson/Sdk/README.md create mode 100644 src/src/ZodSharp/Sdk/README.md rename src/tests/AspNetCore.UnitTests/{AssemblyAttributes.cs => Properties/AssemblyInfo.cs} (100%) delete mode 100644 src/tests/ZodSharp.Benchmarks/README.md rename src/tests/ZodSharp.UnitTests/{Core/AssemblyAttributes.cs => Properties/AssemblyInfo.cs} (100%) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 6e2e244..fb05763 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -8,6 +8,13 @@ "csharpier" ], "rollForward": false + }, + "dotnet-inspect": { + "version": "0.25.0", + "commands": [ + "dotnet-inspect" + ], + "rollForward": false } } } \ No newline at end of file diff --git a/.config/lefthook.yml b/.config/lefthook.yml index 6cc75e8..f005874 100644 --- a/.config/lefthook.yml +++ b/.config/lefthook.yml @@ -1,8 +1,9 @@ -commit-msg: - jobs: - - name: conventional commits - run: npx commitlint --edit {1} pre-commit: jobs: - name: check formatting run: just lint-check + +commit-msg: + jobs: + - name: conventional commits + run: bunx commitlint --edit {1} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f6d25e1 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,117 @@ +# AGENTS.md + +This file is the **primary** instruction set for agents working in this repository. Treat it as the source of truth for behavior, architecture context, testing standards, and completion criteria. + +If another instruction file (for example `.github/copilot-instructions.md`) conflicts with this file, prefer this file unless that file explicitly states an exception. + +## Repository overview + +ZodSharp is a high-performance schema validation library for C#, ported from TypeScript [Zod](https://github.com/colinhacks/zod). It uses struct-based rules and `Span` to minimise allocations, and ships a compile-time source generator for maximum performance. + +- Fork of [guinhx/ZodSharp](https://github.com/guinhx/ZodSharp), maintained at `purview-dev/zodsharp`. +- Public API namespaces are `ZodSharp.*`; packages and assemblies are published under the `Purview.ZodSharp.*` package IDs. +- Multi-targets `net8.0`, `net9.0` and `net10.0`; the source generator targets `netstandard2.0` so it runs in any compiler host. + +## Repository layout + +``` +assets/ Package assets (purview-logo.png) referenced by Directory.Build.props +src/ + ZodSharp.slnx Solution entry point + Directory.Build.props / Directory.Build.targets Purview.DotNetProjectSdk imports + src/ + ZodSharp/ Core validation library + JSON Schema export (Z.ToJsonSchema) + SourceGenerators/ Compile-time [ZodSchema] generator (netstandard2.0, Roslyn) + SystemTextJson/ System.Text.Json integration + JSON Schema import (Z.FromJsonSchema) + NewtonsoftJson/ Newtonsoft.Json integration + JSON Schema import + AspNetCore/ ASP.NET Core ProblemDetails integration + Examples.CLI/ Usage examples + Benchmarks/ BenchmarkDotNet performance suite + tests/ + *.UnitTests/ TUnit test projects (ZodSharp, SystemTextJson, NewtonsoftJson, AspNetCore, SourceGenerators) + cross-platform/ Output directory shared between the C# and TS cross-platform tests +src/ts/ TypeScript (Zod) schema + fixture generation; consumed by C# and vitest tests +tests/ts/ Vitest cross-platform tests +.agents/ Skills/prompts bundled from Purview.DotNetProjectSdk and Purview.SourceGeneratorFramework +``` + +## Build system + +The build is driven by `Purview.DotNetProjectSdk`, imported via `src/Directory.Build.props` (`Sdk.props`) and `src/Directory.Build.targets` (`Sdk.targets`). Key configuration: + +- `NamespacePrefix` is `ZodSharp`. +- Package version comes from `package.json` (SDK version detection). +- Package identities are `Purview.ZodSharp.*`; the SDK derives `AssemblyName`/`PackageId` from `RootNamespace` unless overridden explicitly. +- Package versions are managed centrally in `Directory.Packages.props` (Central Package Management). +- `global.json` pins the `Purview.DotNetProjectSdk` MSBuild SDK version. +- Pack layout for all packages is validated by `purview-build.json` (`PackValidation.RequiredContent`). + +Consult `.agents/skills/sdk-configuration-reference/SKILL.md`, `.agents/skills/sdk-project-behavior-and-detection/SKILL.md`, and `.agents/skills/project-placement-defaults/SKILL.md` before changing build/SDK configuration or adding/moving projects. + +## Commands + +Build and test (see also the `Justfile`): + +```bash +just build # dotnet build src/ZodSharp.slnx -c Debug +just test # dotnet test src/ZodSharp.slnx -c Debug --treenode-filter "/*/*/*/*" +just lint-check # dotnet csharpier check . +just lint-fix # dotnet csharpier format . +just pack # dotnet pack src/ZodSharp.slnx -c Debug -o artifacts +just perf-tests # dotnet run --project src/src/Benchmarks/Benchmarks.csproj -c Debug +just pipeline-pr # PR pipeline via Purview.Build (restore, build, lint, tests) +just pipeline-release # Release pipeline (pack, publish, GitHub release) +``` + +TypeScript-side tooling uses Bun: + +```bash +bun install +bun run test # vitest run (cross-platform TS tests) +bun run generate-fixtures # regenerates src/ts/fixtures/*.json from Zod +``` + +Formatting is enforced with CSharpier; `.editorconfig` at the repo root defines style (tabs for code, 2-space for XML/JSON/YAML/markdown). + +## Testing + +- Test framework is **TUnit** (SDK default; `TUnit` and `TUnit.Mocks` packages in `Directory.Packages.props`). Test projects live under `src/tests/*.UnitTests`. +- Follow the repository's testing bar: + - `[Test]` methods taking a `CancellationToken cancellationToken` parameter. + - `// Arrange`, `// Act`, `// Assert` comments. + - Meaningful, descriptive method names (`Action_GivenCondition_ExpectedResult`). + - Treat work as incomplete until the relevant tests pass. +- For source generators, diagnostic analyzers, code fixes and refactorings, use the `Purview.SourceGeneratorFramework.Testing.TUnit` base classes (`TUnitSourceGeneratorTestBase`, `TUnitDiagnosticAnalyzerTestBase`, `TUnitCodeFixTestBase`, `TUnitRefactoringTestBase`) and assert with `CodeQuery`. Load `.agents/skills/source-generator-testing/SKILL.md` and `.agents/skills/tunit-test-authoring/SKILL.md` before writing or changing these tests. + +## Source generators + +The `[ZodSchema]` attribute (generated into the `ZodSharp` namespace) marks a class or struct for compile-time validator generation. The generator produces a `{TypeName}Schema` static class with `Validate`/`Parse`, optional value-first composition methods (`ApplyAnd`, `ApplyOr`, `ApplyRefine`, controlled by `EnableComposition`, default `true`), and optional `IValidateOptions` support. + +The generator and analyzer are built with `Purview.SourceGeneratorFramework`: + +- Use `CodeWriter` emission and incremental pipelines; load `.agents/skills/source-generator-codewriter-modernization/SKILL.md` before implementing, reviewing, or refactoring generator/analyzer code. +- Keep pipeline values immutable and value-equatable; never retain `ISymbol`, `Compilation`, `SemanticModel`, `IOperation`, `SyntaxNode`, or `Location` in pipeline models. +- Use `ForAttributeWithMetadataName` for attribute-driven discovery. +- Test incrementally, not just generated text (see the skills above). + +## Packing and package READMEs + +Each package ships its own `README.md`, placed in the project's `Sdk/` folder (for example `src/src/ZodSharp/Sdk/README.md`). The SDK's `PurviewAutoSdkPack` automatically maps `Sdk/*.md` to the package root and `Sdk/buildTransitive/**` to `buildTransitive/`, and the repo-root `README.md` is skipped when a package already packs its own README. Packages also ship `purview-logo.png` (linked via `src/Directory.Build.props`) and the core package ships `buildTransitive/Purview.ZodSharp.props`. + +Keep `purview-build.json`'s `PackValidation` requirements in sync with any packaging change. + +## Cross-platform fixtures + +Schemas and fixtures shared between TypeScript/Zod and C#/ZodSharp live under `src/ts/`. The fixture generator (`bun run generate-fixtures`) writes JSON fixtures and a `manifest.json` that the C# tests consume; the C# cross-platform tests write output to `src/tests/cross-platform/output/`, which the vitest tests (`tests/ts/cross-platform.test.ts`) read. Keep the TS `UserSchema` and the C# `CrossPlatformUserSchema` in sync. + +## Commit conventions + +Commits must follow [Conventional Commits](https://www.conventionalcommits.org/), enforced by Lefthook + Commitlint (see `.config/lefthook.yml` and `commitlint.config.mts`). Allowed types: `build`, `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `revert`, `style`, `test`. + +## Quality gates + +- `just build` succeeds with no new warnings/errors. +- Relevant tests pass. +- `just lint-check` (CSharpier) reports no formatting changes. +- Generated code is deterministic and reviewable; no scope leaks in `CodeWriter` output. +- Packed packages match `purview-build.json` `PackValidation` (including per-package `README.md` and `purview-logo.png`). diff --git a/Directory.Packages.props b/Directory.Packages.props index 9ff5446..9a1c0fb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,13 +5,13 @@ true 5.9.0 5.9.0 1.66.27 - 1.0.0-prerelease.39 + 1.0.0-prerelease.42 10.0.12 diff --git a/Justfile b/Justfile index 4f7f7f8..0653239 100644 --- a/Justfile +++ b/Justfile @@ -2,13 +2,13 @@ set quiet root_folder := "src" solution := root_folder / "ZodSharp.slnx" -perf_tests_project := root_folder / "tests" / "ZodSharp.PerformanceTests" / "ZodSharp.PerformanceTests.csproj" +perf_tests_project := root_folder / "src" / "Benchmarks" / "Benchmarks.csproj" build_configuration := "Debug" artifacts_folder := "./artifacts" -default_test_filter := "/*/*/*/*/" +default_test_filter := "/*/*/*/*" pipeline_feed := "https://api.nuget.org/v3/index.json" -pipeline_tool := ".tools/purview-build/purview-build" +pipeline_tool := ".tools/purview-build/purview-build" + if os() == "windows" { ".exe" } else { "" } [private] default: @@ -60,31 +60,31 @@ pipeline-tests *args: echo "Running tests pipeline..." "{{ pipeline_tool }}" --Build:RunTests=true --Release:Mode=None {{ args }} -# Build and test with the specified configuration, defaulting to "Release" +# Build and test with the specified configuration, defaulting to "Debug" [group('Build and Test')] build *args: echo "Building {{ BLUE }}{{ solution }}{{ NORMAL }} with configuration {{ YELLOW }}{{ build_configuration }}{{ NORMAL }}" dotnet build {{ solution }} -c {{ build_configuration }} {{ args }} -# Build and test with the specified configuration, defaulting to "Release" +# Clean the solution with the specified configuration, defaulting to "Debug" [group('Build and Test')] clean *args: echo "Cleaning {{ BLUE }}{{ solution }}{{ NORMAL }} with configuration {{ YELLOW }}{{ build_configuration }}{{ NORMAL }}" dotnet clean {{ solution }} -c {{ build_configuration }} {{ args }} -# Run the performance tests with the specified configuration, defaulting to "Release" +# Run the performance benchmarks with the specified configuration, defaulting to "Debug" [group('Build and Test')] perf-tests *args: - echo "Running performance tests for {{ BLUE }}{{ perf_tests_project }}{{ NORMAL }} with configuration {{ YELLOW }}{{ build_configuration }}{{ NORMAL }}" - dotnet run --project {{ perf_tests_project }} -c {{ build_configuration }} {{ args }} + echo "Running performance tests for {{ BLUE }}{{ perf_tests_project }}{{ NORMAL }}" + dotnet run --project {{ perf_tests_project }} -c Release {{ args }} -# Run tests with the specified configuration, defaulting to "Release" +# Run tests with the specified configuration, defaulting to "Debug" [group('Build and Test')] test filter=default_test_filter *args: echo "Running tests for {{ BLUE }}{{ solution }}{{ NORMAL }} with configuration {{ YELLOW }}{{ build_configuration }}{{ NORMAL }} and filter {{ GREEN }}{{ filter }}{{ NORMAL }}" dotnet test {{ solution }} -c {{ build_configuration }} --treenode-filter "{{ filter }}" {{ args }} -# Run tests with the specified configuration, defaulting to "Release" +# Restore dependencies for the solution [group('Build and Test')] restore *args: echo "Restoring dependencies for {{ BLUE }}{{ solution }}{{ NORMAL }}" diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 300680a..0000000 --- a/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -MIT License - -Copyright (c) 2025 Colin McDonnell -Copyright (c) 2025 Diego Santos de Araújo Teles - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index b7c9c00..639facb 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,11 @@ # ZodSharp [![NuGet version](https://img.shields.io/nuget/v/Purview.ZodSharp.svg)](https://www.nuget.org/packages/Purview.ZodSharp) +[![Release](https://github.com/purview-dev/zodsharp/actions/workflows/release.yml/badge.svg)](https://github.com/purview-dev/zodsharp/actions/workflows/release.yml) **ZodSharp** is a high-performance schema validation library for C#, ported from TypeScript [Zod](https://github.com/colinhacks/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](https://github.com/guinhx/ZodSharp), maintained at [github.com/purview-dev/ZodSharp](https://github.com/purview-dev/ZodSharp) under the `Purview.*` package IDs. - -[![Release](https://github.com/purview-dev/ZodSharp/actions/workflows/release.yml/badge.svg)](https://github.com/purview-dev/ZodSharp/actions/workflows/release.yml) +This project is a fork of [guinhx/ZodSharp](https://github.com/guinhx/ZodSharp), maintained at [github.com/purview-dev/zodsharp](https://github.com/purview-dev/zodsharp) under the `Purview.*` package IDs. ## Key Features @@ -50,10 +49,10 @@ dotnet add package Purview.ZodSharp.AspNetCore ``` -- **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. +- **Purview.ZodSharp** — Core validation library and source generator (`[ZodSchema]`). See its [package README](src/src/ZodSharp/Sdk/README.md). +- **Purview.ZodSharp.SystemTextJson** — System.Text.Json integration and JSON Schema import. See its [package README](src/src/SystemTextJson/Sdk/README.md). +- **Purview.ZodSharp.NewtonsoftJson** — Newtonsoft.Json integration and JSON Schema import. See its [package README](src/src/NewtonsoftJson/Sdk/README.md). +- **Purview.ZodSharp.AspNetCore** — ASP.NET Core ProblemDetails integration. See its [package README](src/src/AspNetCore/Sdk/README.md). ## TypeScript and fixture tooling @@ -232,7 +231,7 @@ ZodSharp implements several optimizations for maximum performance: - Validation rules implemented as `struct` to avoid allocations - Use of `Span` and `ReadOnlySpan` when appropriate -- Object pooling for reusable schemas +- Array pooling via `ArrayPool` for zero-allocation helpers #### 2. Struct-based Rules @@ -270,14 +269,14 @@ var schema = Z.String() ### Performance Benchmarks -We maintain comprehensive performance tests in `src/tests/ZodSharp.Benchmarks`. Run them yourself: +We maintain comprehensive performance tests in `src/src/Benchmarks`. Run them yourself: ```bash # Run all performance benchmarks -dotnet run --project src/tests/ZodSharp.Benchmarks/ZodSharp.Benchmarks.csproj -c Release +dotnet run --project src/src/Benchmarks/Benchmarks.csproj -c Release # Run specific test suites -dotnet run --project src/tests/ZodSharp.Benchmarks/ZodSharp.Benchmarks.csproj -c Release --filter "*MemoryPerformanceTests*" +dotnet run --project src/src/Benchmarks/Benchmarks.csproj -c Release --filter "*MemoryPerformanceTests*" ``` **Key performance highlights**: @@ -288,22 +287,21 @@ dotnet run --project src/tests/ZodSharp.Benchmarks/ZodSharp.Benchmarks.csproj -c - **Minimal GC pressure** with struct-based architecture - **Scalable** performance even with complex nested schemas -See the [performance README](src/tests/ZodSharp.Benchmarks/README.md) for detailed benchmark results and optimization tips. +See the [performance README](src/src/Benchmarks/README.md) 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 +├── src/ +│ ├── ZodSharp/ # Core validation library (Core, Schemas, Rules, JsonSchema) +│ ├── 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 +│ └── Benchmarks/ # BenchmarkDotNet performance suite +└── tests/ # TUnit test projects ``` 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`. @@ -368,7 +366,7 @@ ZodSharp ships separate integration packages for the two major .NET JSON librari #### System.Text.Json (`Purview.ZodSharp.SystemTextJson`) ```csharp -using ZodSharp.Json; +using ZodSharp; // Deserialize and validate from string var result = schema.DeserializeAndValidate(jsonString); @@ -383,7 +381,7 @@ var converter = schema.CreateValidatingConverter(); #### Newtonsoft.Json (`Purview.ZodSharp.NewtonsoftJson`) ```csharp -using ZodSharp.Json; +using ZodSharp; // Deserialize and validate from string var result = schema.DeserializeAndValidate(jsonString); @@ -505,7 +503,7 @@ Generate zero-allocation validators at compile time: ```csharp using System.ComponentModel.DataAnnotations; -using ZodSharp.SourceGenerators; +using ZodSharp; [ZodSchema] public class User @@ -623,7 +621,7 @@ Package versions are declared centrally in `Directory.Packages.props`. No `packa ## License -MIT License - see the LICENSE file for details. +MIT — the license is declared in the NuGet package metadata (`PackageLicenseExpression`) and in `package.json`. ## Contributing diff --git a/assets/images/purview-logo.png b/assets/images/purview-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3eee1e900d90c0844319e6742631716cb7f8bcbf GIT binary patch literal 41065 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

H+HJ2da1uU3`0qut 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.4.2" }, "packageManager": "bun@1.4.2" -} +} \ No newline at end of file diff --git a/purview-build.json b/purview-build.json index 1829240..1b4fe2f 100644 --- a/purview-build.json +++ b/purview-build.json @@ -3,7 +3,7 @@ "Solution": "src/ZodSharp.slnx", "TestRoot": "src/tests", "TestPatterns": "*Tests.csproj", - "TestFilter": "/*/*/*/*/" + "TestFilter": "/*/*/*/*" }, "PackValidation": { "RequireSymbolPackage": true, @@ -19,7 +19,9 @@ "analyzers/dotnet/cs/Purview.SourceGeneratorFramework.dll", "analyzers/dotnet/cs/Purview.SourceGeneratorFramework.Shared.dll", "analyzers/dotnet/cs/Purview.ZodSharp.SourceGenerators.dll", - "buildTransitive/Purview.ZodSharp.props" + "buildTransitive/Purview.ZodSharp.props", + "README.md", + "purview-logo.png" ], "purview.zodsharp.aspnetcore": [ "lib/net10.0/Purview.ZodSharp.AspNetCore.dll", @@ -27,7 +29,9 @@ "lib/net9.0/Purview.ZodSharp.AspNetCore.dll", "lib/net9.0/Purview.ZodSharp.AspNetCore.xml", "lib/net8.0/Purview.ZodSharp.AspNetCore.dll", - "lib/net8.0/Purview.ZodSharp.AspNetCore.xml" + "lib/net8.0/Purview.ZodSharp.AspNetCore.xml", + "README.md", + "purview-logo.png" ], "purview.zodsharp.newtonsoftjson": [ "lib/net10.0/Purview.ZodSharp.NewtonsoftJson.dll", @@ -35,7 +39,9 @@ "lib/net9.0/Purview.ZodSharp.NewtonsoftJson.dll", "lib/net9.0/Purview.ZodSharp.NewtonsoftJson.xml", "lib/net8.0/Purview.ZodSharp.NewtonsoftJson.dll", - "lib/net8.0/Purview.ZodSharp.NewtonsoftJson.xml" + "lib/net8.0/Purview.ZodSharp.NewtonsoftJson.xml", + "README.md", + "purview-logo.png" ], "purview.zodsharp.systemtextjson": [ "lib/net10.0/Purview.ZodSharp.SystemTextJson.dll", @@ -43,7 +49,9 @@ "lib/net9.0/Purview.ZodSharp.SystemTextJson.dll", "lib/net9.0/Purview.ZodSharp.SystemTextJson.xml", "lib/net8.0/Purview.ZodSharp.SystemTextJson.dll", - "lib/net8.0/Purview.ZodSharp.SystemTextJson.xml" + "lib/net8.0/Purview.ZodSharp.SystemTextJson.xml", + "README.md", + "purview-logo.png" ] }, "ForbiddenContent": {} diff --git a/src/Directory.Build.props b/src/Directory.Build.props index ad49251..6c1aec5 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -23,7 +23,18 @@ ZodSharp Contributors;Purview-Dev Contributors https://github.com/purview-dev/zodsharp - MIT + Purview.dev + $(RepositoryUrl) + $(RepositoryUrl) + git + purview-logo.png README.md + MIT + false + + + + + diff --git a/src/ZodSharp.slnx b/src/ZodSharp.slnx index 140a3ec..4cd45bc 100644 --- a/src/ZodSharp.slnx +++ b/src/ZodSharp.slnx @@ -16,7 +16,6 @@ - @@ -25,5 +24,6 @@ + diff --git a/src/src/AspNetCore/AspNetCore.csproj b/src/src/AspNetCore/AspNetCore.csproj index 8d07f92..5061190 100644 --- a/src/src/AspNetCore/AspNetCore.csproj +++ b/src/src/AspNetCore/AspNetCore.csproj @@ -13,8 +13,4 @@ - - - - diff --git a/src/src/AspNetCore/Sdk/README.md b/src/src/AspNetCore/Sdk/README.md new file mode 100644 index 0000000..2ee0d1d --- /dev/null +++ b/src/src/AspNetCore/Sdk/README.md @@ -0,0 +1,47 @@ +# Purview.ZodSharp.AspNetCore + +[![NuGet version](https://img.shields.io/nuget/v/Purview.ZodSharp.AspNetCore.svg)](https://www.nuget.org/packages/Purview.ZodSharp.AspNetCore) +[![Release](https://github.com/purview-dev/zodsharp/actions/workflows/release.yml/badge.svg)](https://github.com/purview-dev/zodsharp/actions/workflows/release.yml) + +ASP.NET Core integration for [Purview.ZodSharp](https://www.nuget.org/packages/Purview.ZodSharp) [![NuGet version](https://img.shields.io/nuget/v/Purview.ZodSharp.svg)](https://www.nuget.org/packages/Purview.ZodSharp). Convert failed validation results into standard `ProblemDetails` / `HttpValidationProblemDetails` payloads while preserving the structured validation issues. + +## Installation + +```bash +dotnet add package Purview.ZodSharp.AspNetCore +``` + +## Usage + +```csharp +using ZodSharp.AspNetCore; + +var result = BasketSchema.Validate(basket); + +if (!result.IsSuccess) +{ + var problem = result.ToHttpValidationProblemDetails(); + return Results.ValidationProblem( + problem.Errors, + extensions: new Dictionary + { + ["issues"] = problem.Extensions["issues"], + }); +} +``` + +`ToHttpValidationProblemDetails()` maps each `ValidationError` to a standard `HttpValidationProblemDetails.Errors` entry keyed by its JSON path. The structured `ValidationIssue` metadata (code, origin, minimum/maximum, inclusive bounds, path, message) is preserved in the `issues` extension so clients get more than a flat message list. + +A `ValidationProblemDetails` overload is also available: + +```csharp +var problem = result.ToValidationProblemDetails(); +``` + +## Further reading + +For the full API, cross-platform TypeScript/Zod interop, and performance notes see the [repository README](https://github.com/purview-dev/zodsharp/blob/main/README.md). + +## License + +MIT — see the package metadata in `Purview.ZodSharp.AspNetCore` on NuGet. diff --git a/src/tests/ZodSharp.Benchmarks/ArrayPerformanceTests.cs b/src/src/Benchmarks/ArrayPerformanceTests.cs similarity index 100% rename from src/tests/ZodSharp.Benchmarks/ArrayPerformanceTests.cs rename to src/src/Benchmarks/ArrayPerformanceTests.cs diff --git a/src/tests/ZodSharp.Benchmarks/BasicPerformanceTests.cs b/src/src/Benchmarks/BasicPerformanceTests.cs similarity index 100% rename from src/tests/ZodSharp.Benchmarks/BasicPerformanceTests.cs rename to src/src/Benchmarks/BasicPerformanceTests.cs diff --git a/src/tests/ZodSharp.Benchmarks/ZodSharp.Benchmarks.csproj b/src/src/Benchmarks/Benchmarks.csproj similarity index 62% rename from src/tests/ZodSharp.Benchmarks/ZodSharp.Benchmarks.csproj rename to src/src/Benchmarks/Benchmarks.csproj index 48266dc..b01a802 100644 --- a/src/tests/ZodSharp.Benchmarks/ZodSharp.Benchmarks.csproj +++ b/src/src/Benchmarks/Benchmarks.csproj @@ -1,16 +1,17 @@  net10.0 - - false + enable + enable ZodSharp $(NoWarn);CA1303;CA1707;CA1822;CA1515; Exe - Purview.ZodSharp.PerformanceBenchmark + + Benchmarks - + diff --git a/src/src/Benchmarks/Directory.Build.props b/src/src/Benchmarks/Directory.Build.props new file mode 100644 index 0000000..4344d48 --- /dev/null +++ b/src/src/Benchmarks/Directory.Build.props @@ -0,0 +1,19 @@ + + + diff --git a/src/src/Benchmarks/Directory.Build.targets b/src/src/Benchmarks/Directory.Build.targets new file mode 100644 index 0000000..ca987a3 --- /dev/null +++ b/src/src/Benchmarks/Directory.Build.targets @@ -0,0 +1,10 @@ + + + diff --git a/src/tests/ZodSharp.Benchmarks/HeavyPerformanceTests.cs b/src/src/Benchmarks/HeavyPerformanceTests.cs similarity index 100% rename from src/tests/ZodSharp.Benchmarks/HeavyPerformanceTests.cs rename to src/src/Benchmarks/HeavyPerformanceTests.cs diff --git a/src/tests/ZodSharp.Benchmarks/MemoryPerformanceTests.cs b/src/src/Benchmarks/MemoryPerformanceTests.cs similarity index 100% rename from src/tests/ZodSharp.Benchmarks/MemoryPerformanceTests.cs rename to src/src/Benchmarks/MemoryPerformanceTests.cs diff --git a/src/tests/ZodSharp.Benchmarks/ObjectPerformanceTests.cs b/src/src/Benchmarks/ObjectPerformanceTests.cs similarity index 100% rename from src/tests/ZodSharp.Benchmarks/ObjectPerformanceTests.cs rename to src/src/Benchmarks/ObjectPerformanceTests.cs diff --git a/src/tests/ZodSharp.Benchmarks/Program.cs b/src/src/Benchmarks/Program.cs similarity index 100% rename from src/tests/ZodSharp.Benchmarks/Program.cs rename to src/src/Benchmarks/Program.cs diff --git a/src/src/Benchmarks/README.md b/src/src/Benchmarks/README.md new file mode 100644 index 0000000..4b3c38f --- /dev/null +++ b/src/src/Benchmarks/README.md @@ -0,0 +1,84 @@ +# ZodSharp Performance Benchmarks + +BenchmarkDotNet performance suite for ZodSharp. This is a `net10.0` console application; every suite uses `[MemoryDiagnoser]` plus a `[SimpleJob]` profile so time and allocation data are captured together. + +## Structure + +| File | Covers | +| --- | --- | +| `BasicPerformanceTests.cs` | Primitive validations — string, number, boolean, small string arrays, and multi-rule chains on strings and numbers | +| `ObjectPerformanceTests.cs` | Object validation — a 2-field simple object, a 6-field medium object, a ~13-field complex object with nested data, and an invalid complex object | +| `ArrayPerformanceTests.cs` | Arrays of different sizes — small (10), medium (100) and large (1000) string arrays, number arrays, arrays with complex schemas, and invalid large arrays | +| `HeavyPerformanceTests.cs` | Stress cases — a 4-level deeply nested object, a 50-field wide object, nested jagged arrays, a string with many chained refinements, and a large object containing arrays | +| `MemoryPerformanceTests.cs` | Allocation-focused benchmarks for string, array and object validation (uses more iterations to surface allocation differences) | +| `TransformPerformanceTests.cs` | Transformations — `ToLower`, `ToUpper`, `Trim`, chained transforms, and a transform combined with validation | +| `UnionPerformanceTests.cs` | Union and discriminated-union validation, including invalid inputs | + +## How to run + +From the repository root: + +```bash +# Run the full benchmark suite +dotnet run --project src/src/Benchmarks/Benchmarks.csproj -c Release + +# Or via just +just perf-tests +``` + +Run a single suite with `--filter`: + +```bash +dotnet run --project src/src/Benchmarks/Benchmarks.csproj -c Release --filter "*BasicPerformanceTests*" +dotnet run --project src/src/Benchmarks/Benchmarks.csproj -c Release --filter "*MemoryPerformanceTests*" +``` + +Always use `-c Release`; running benchmarks in Debug produces meaningless numbers. + +## Results + +Benchmark results are written to the `BenchmarkDotNet.Artifacts` folder in the project directory: + +- **HTML reports** — interactive per-benchmark results +- **Markdown reports** — summary tables suitable for commit/PR comments +- **Logs** — detailed execution logs + +## Interpreting results + +Key metrics reported per benchmark: + +1. **Mean** — average execution time +2. **Error / StdDev** — noise and spread of the timings +3. **Gen 0/1/2** — garbage collections per generation per 1000 operations +4. **Allocated** — bytes allocated per operation + +Signs to investigate: + +- **High time** — look for unnecessary work in hot paths +- **High allocations** — the zero-allocation claim breaks; check for boxing, LINQ, or closure captures in the rule chain +- **Gen 2 collections** — long-lived allocations; a sign of avoidable large-object churn + +## Adding a new benchmark suite + +1. Add a `*.cs` file under `src/src/Benchmarks/`. +2. Mark the class with `[MemoryDiagnoser]` and a `[SimpleJob]` profile. +3. Build the schema and test data in the constructor (setup runs once per benchmark process). +4. Add one `[Benchmark]` method per scenario, returning the `ValidationResult` (BenchmarkDotNet treats the return value as the "operation" — do not discard it). + +```csharp +using BenchmarkDotNet.Attributes; +using ZodSharp; +using ZodSharp.Core; + +[MemoryDiagnoser] +[SimpleJob(launchCount: 1, warmupCount: 3, iterationCount: 5)] +public class MyPerformanceTests +{ + readonly ZodString _schema = Z.String().Min(3).Max(50); + + [Benchmark] + public ValidationResult ValidateValidValue() => _schema.Validate("user@example.com"); +} +``` + +Run the suite with `--filter "*MyPerformanceTests*"` and confirm the class appears in the `BenchmarkDotNet.Artifacts` output. diff --git a/src/tests/ZodSharp.Benchmarks/TransformPerformanceTests.cs b/src/src/Benchmarks/TransformPerformanceTests.cs similarity index 100% rename from src/tests/ZodSharp.Benchmarks/TransformPerformanceTests.cs rename to src/src/Benchmarks/TransformPerformanceTests.cs diff --git a/src/tests/ZodSharp.Benchmarks/UnionPerformanceTests.cs b/src/src/Benchmarks/UnionPerformanceTests.cs similarity index 100% rename from src/tests/ZodSharp.Benchmarks/UnionPerformanceTests.cs rename to src/src/Benchmarks/UnionPerformanceTests.cs diff --git a/src/src/NewtonsoftJson/NewtonsoftJson.csproj b/src/src/NewtonsoftJson/NewtonsoftJson.csproj index 4390f0a..ddfe724 100644 --- a/src/src/NewtonsoftJson/NewtonsoftJson.csproj +++ b/src/src/NewtonsoftJson/NewtonsoftJson.csproj @@ -20,8 +20,4 @@ - - - - diff --git a/src/src/NewtonsoftJson/Sdk/README.md b/src/src/NewtonsoftJson/Sdk/README.md new file mode 100644 index 0000000..0be7b44 --- /dev/null +++ b/src/src/NewtonsoftJson/Sdk/README.md @@ -0,0 +1,63 @@ +# Purview.ZodSharp.NewtonsoftJson + +[![NuGet version](https://img.shields.io/nuget/v/Purview.ZodSharp.NewtonsoftJson.svg)](https://www.nuget.org/packages/Purview.ZodSharp.NewtonsoftJson) +[![Release](https://github.com/purview-dev/zodsharp/actions/workflows/release.yml/badge.svg)](https://github.com/purview-dev/zodsharp/actions/workflows/release.yml) + +Newtonsoft.Json integration for [Purview.ZodSharp](https://www.nuget.org/packages/Purview.ZodSharp) [![NuGet version](https://img.shields.io/nuget/v/Purview.ZodSharp.svg)](https://www.nuget.org/packages/Purview.ZodSharp). Deserialize-and-validate JSON in a single pass, create validating `JsonConverter` instances, and import JSON Schema definitions (`Z.FromJsonSchema`). + +## Installation + +```bash +dotnet add package Purview.ZodSharp.NewtonsoftJson +``` + +## Deserialize and validate + +```csharp +using ZodSharp; + +var userSchema = Z.Object() + .Field("name", Z.String().Min(3)) + .Field("age", Z.Number().Min(0).Int()) + .Build(); + +var json = """{ "name": "John", "age": 30 }"""; + +var result = userSchema.DeserializeAndValidate(json); +if (result.IsSuccess) + Console.WriteLine($"Valid: {result.Value}"); +``` + +Async stream overload and `JToken` overload: + +```csharp +await using var stream = File.OpenRead("user.json"); +var result = await userSchema.DeserializeAndValidateAsync(stream); + +var jToken = JObject.Parse(json); +var result2 = userSchema.DeserializeAndValidate(jToken); +``` + +## Validating converter + +```csharp +var converter = userSchema.CreateValidatingConverter(); +var value = JsonConvert.DeserializeObject(json, converter); +``` + +## Import from JSON Schema + +```csharp +var schema = Z.FromJsonSchema(jsonSchemaString); +var result = schema.Validate(data); +``` + +This lets you share schemas defined in TypeScript/Zod with your .NET backend. (Export via `Z.ToJsonSchema` lives in the core package.) + +## Further reading + +For the full API, cross-platform TypeScript/Zod interop, and performance notes see the [repository README](https://github.com/purview-dev/zodsharp/blob/main/README.md). + +## License + +MIT — see the package metadata in `Purview.ZodSharp.NewtonsoftJson` on NuGet. diff --git a/src/src/SourceGenerators/AnalyzerReleases.Shipped.md b/src/src/SourceGenerators/AnalyzerReleases.Shipped.md index 06b80d0..dae3318 100644 --- a/src/src/SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/src/SourceGenerators/AnalyzerReleases.Shipped.md @@ -20,12 +20,12 @@ | ZODSGEN008 | ZodSharp.SourceGenerator | Error | Invalid return type for custom validation method | | ZODSGEN009 | ZodSharp.SourceGenerator | Error | Invalid custom validation method parameter count | | ZODSGEN010 | ZodSharp.SourceGenerator | Error | Invalid custom validation model parameter | -| ZODSGEN011 | ZodSharp.SourceGenerator | Error | Missing or invalid CancellationToken parmaeter | +| ZODSGEN011 | ZodSharp.SourceGenerator | Error | Missing or invalid CancellationToken parameter | | ZODSGEN012 | ZodSharp.SourceGenerator | Error | Generic custom validation method unsupported | | ZODSGEN013 | ZodSharp.SourceGenerator | Error | Custom validation method must be static when defined on model type | | ZODSGEN014 | ZodSharp.SourceGenerator | Error | Inaccessible custom validation method | | ZODSGEN015 | ZodSharp.SourceGenerator | Error | Ambiguous custom validation method overloads | | ZODSGEN016 | ZodSharp.SourceGenerator | Error | Invalid custom validation method name | -| ZODSGEN017 | ZodSharp.SourceGenerator | Error | Invalid abstract custom validation method unsupported | +| ZODSGEN017 | ZodSharp.SourceGenerator | Error | Abstract custom validation method unsupported | | ZODSGEN018 | ZodSharp.SourceGenerator | Error | Partial custom validation method has not been implemented | -| ZODSGEN019 | ZodSharp.SourceGenerator | Error | Invalidte custom validation method parameter modifier | +| ZODSGEN019 | ZodSharp.SourceGenerator | Error | Invalid custom validation method parameter modifier | diff --git a/src/src/SourceGenerators/Helpers/SourceGenLibrary.cs b/src/src/SourceGenerators/Helpers/SourceGenLibrary.cs index a76b0ea..9a347a9 100644 --- a/src/src/SourceGenerators/Helpers/SourceGenLibrary.cs +++ b/src/src/SourceGenerators/Helpers/SourceGenLibrary.cs @@ -186,6 +186,7 @@ CancellationToken cancellationToken customValidation, syncValidation, generateIValidateOptions, + zodSchemaAttribute.EnableComposition, isPrimary ) ); diff --git a/src/src/SourceGenerators/Models/SchemaGenerationModel.cs b/src/src/SourceGenerators/Models/SchemaGenerationModel.cs index b5a522e..972c68f 100644 --- a/src/src/SourceGenerators/Models/SchemaGenerationModel.cs +++ b/src/src/SourceGenerators/Models/SchemaGenerationModel.cs @@ -39,6 +39,7 @@ enum PropertyValidationKind /// The custom validation method data, if any. /// The synchronous refinement method data, if any. /// Requested IValidateOptions generation: null = auto, true = force, false = opt out. +/// Whether the value-first composition methods (ApplyAnd/ApplyOr/ApplyRefine) are generated. /// True if this is the primary schema for the target type, false if it is a secondary schema. readonly record struct ZodSchemaDescriptor( TypeIdentity TargetType, @@ -51,6 +52,7 @@ readonly record struct ZodSchemaDescriptor( GeneratorResult CustomValidationMethod, GeneratorResult SyncValidationMethod, bool? GenerateIValidateOptions, + bool EnableComposition, bool IsPrimary ); diff --git a/src/src/SourceGenerators/Models/ZodSchemaAttributeData.cs b/src/src/SourceGenerators/Models/ZodSchemaAttributeData.cs index 8a64e80..b31e3a8 100644 --- a/src/src/SourceGenerators/Models/ZodSchemaAttributeData.cs +++ b/src/src/SourceGenerators/Models/ZodSchemaAttributeData.cs @@ -5,7 +5,7 @@ readonly partial record struct ZodSchemaAttributeData( string? SchemaName, [Property(DefaultValue = true)] bool GenerateValidateMethod, [Property(DefaultValue = true)] bool GenerateParseMethod, - [Property(DefaultValue = false)] bool EnableComposition, + [Property(DefaultValue = true)] bool EnableComposition, string? CustomValidationMethodName, string? RefinementMethodName, [Property(DefaultValue = false)] bool GenerateIValidateOptions, diff --git a/src/src/SourceGenerators/SourceGenerators.csproj b/src/src/SourceGenerators/SourceGenerators.csproj index 4a6d97e..9bed6b8 100644 --- a/src/src/SourceGenerators/SourceGenerators.csproj +++ b/src/src/SourceGenerators/SourceGenerators.csproj @@ -2,8 +2,8 @@ true Purview.ZodSharp.SourceGenerators - true - true + diff --git a/src/src/SourceGenerators/ZodSchemaGenerator.BuildSchema.cs b/src/src/SourceGenerators/ZodSchemaGenerator.BuildSchema.cs index 6d1f6a0..42dcb63 100644 --- a/src/src/SourceGenerators/ZodSchemaGenerator.BuildSchema.cs +++ b/src/src/SourceGenerators/ZodSchemaGenerator.BuildSchema.cs @@ -97,7 +97,8 @@ CancellationToken cancellationToken GenerateValidateMethod(writer, outputContext.ZodSchema, cancellationToken); GenerateParseMethod(writer, outputContext.ZodSchema, cancellationToken); - GenerateCompositionMethods(writer, outputContext.ZodSchema, cancellationToken); + if (outputContext.ZodSchema.EnableComposition) + GenerateCompositionMethods(writer, outputContext.ZodSchema, cancellationToken); } if (isPrimary) diff --git a/src/src/SystemTextJson/Sdk/README.md b/src/src/SystemTextJson/Sdk/README.md new file mode 100644 index 0000000..2652910 --- /dev/null +++ b/src/src/SystemTextJson/Sdk/README.md @@ -0,0 +1,61 @@ +# Purview.ZodSharp.SystemTextJson + +[![NuGet version](https://img.shields.io/nuget/v/Purview.ZodSharp.SystemTextJson.svg)](https://www.nuget.org/packages/Purview.ZodSharp.SystemTextJson) +[![Release](https://github.com/purview-dev/zodsharp/actions/workflows/release.yml/badge.svg)](https://github.com/purview-dev/zodsharp/actions/workflows/release.yml) + +System.Text.Json integration for [Purview.ZodSharp](https://www.nuget.org/packages/Purview.ZodSharp) [![NuGet version](https://img.shields.io/nuget/v/Purview.ZodSharp.svg)](https://www.nuget.org/packages/Purview.ZodSharp). Deserialize-and-validate JSON in a single pass, create validating `JsonConverter` instances, and import JSON Schema definitions (`Z.FromJsonSchema`). + +## Installation + +```bash +dotnet add package Purview.ZodSharp.SystemTextJson +``` + +## Deserialize and validate + +```csharp +using ZodSharp; + +var userSchema = Z.Object() + .Field("name", Z.String().Min(3)) + .Field("age", Z.Number().Min(0).Int()) + .Build(); + +var json = """{ "name": "John", "age": 30 }"""; + +var result = userSchema.DeserializeAndValidate(json); +if (result.IsSuccess) + Console.WriteLine($"Valid: {result.Value}"); +``` + +Async stream overload: + +```csharp +await using var stream = File.OpenRead("user.json"); +var result = await userSchema.DeserializeAndValidateAsync(stream); +``` + +## Validating converter + +```csharp +var converter = userSchema.CreateValidatingConverter(); +var options = new JsonSerializerOptions { Converters = { converter } }; +var value = JsonSerializer.Deserialize(json, options); +``` + +## Import from JSON Schema + +```csharp +var schema = Z.FromJsonSchema(jsonSchemaString); +var result = schema.Validate(data); +``` + +This lets you share schemas defined in TypeScript/Zod with your .NET backend. (Export via `Z.ToJsonSchema` lives in the core package.) + +## Further reading + +For the full API, cross-platform TypeScript/Zod interop, and performance notes see the [repository README](https://github.com/purview-dev/zodsharp/blob/main/README.md). + +## License + +MIT — see the package metadata in `Purview.ZodSharp.SystemTextJson` on NuGet. diff --git a/src/src/SystemTextJson/SystemTextJson.csproj b/src/src/SystemTextJson/SystemTextJson.csproj index d91d65e..6bc87d9 100644 --- a/src/src/SystemTextJson/SystemTextJson.csproj +++ b/src/src/SystemTextJson/SystemTextJson.csproj @@ -16,8 +16,4 @@ - - - - diff --git a/src/src/ZodSharp/Sdk/README.md b/src/src/ZodSharp/Sdk/README.md new file mode 100644 index 0000000..9a019ea --- /dev/null +++ b/src/src/ZodSharp/Sdk/README.md @@ -0,0 +1,85 @@ +# Purview.ZodSharp + +[![NuGet version](https://img.shields.io/nuget/v/Purview.ZodSharp.svg)](https://www.nuget.org/packages/Purview.ZodSharp) +[![Release](https://github.com/purview-dev/zodsharp/actions/workflows/release.yml/badge.svg)](https://github.com/purview-dev/zodsharp/actions/workflows/release.yml) + +A high-performance schema validation library for C#, ported from TypeScript [Zod](https://github.com/colinhacks/zod). It features zero-allocation validation, struct-based rules, a fluent API, and a compile-time source generator for maximum performance. + +This package is the core library. It also ships the `[ZodSchema]` source generator and JSON Schema **export** (`Z.ToJsonSchema`). JSON Schema **import** (`Z.FromJsonSchema`) and JSON serialization integrations are available in the companion packages: + +- [Purview.ZodSharp.SystemTextJson](https://www.nuget.org/packages/Purview.ZodSharp.SystemTextJson) [![NuGet version](https://img.shields.io/nuget/v/Purview.ZodSharp.SystemTextJson.svg)](https://www.nuget.org/packages/Purview.ZodSharp.SystemTextJson) +- [Purview.ZodSharp.NewtonsoftJson](https://www.nuget.org/packages/Purview.ZodSharp.NewtonsoftJson) [![NuGet version](https://img.shields.io/nuget/v/Purview.ZodSharp.NewtonsoftJson.svg)](https://www.nuget.org/packages/Purview.ZodSharp.NewtonsoftJson) +- [Purview.ZodSharp.AspNetCore](https://www.nuget.org/packages/Purview.ZodSharp.AspNetCore) [![NuGet version](https://img.shields.io/nuget/v/Purview.ZodSharp.AspNetCore.svg)](https://www.nuget.org/packages/Purview.ZodSharp.AspNetCore) + +## Installation + +```bash +dotnet add package Purview.ZodSharp +``` + +## Quick start + +```csharp +using ZodSharp; + +var nameSchema = Z.String().Min(3).Max(50); +var result = nameSchema.Validate("John"); + +if (result.IsSuccess) + Console.WriteLine($"Valid name: {result.Value}"); +``` + +Composable schemas for objects, arrays, unions, discriminators, records, tuples and more: + +```csharp +var userSchema = Z.Object() + .Field("name", Z.String().Min(1)) + .Field("age", Z.Number().Min(0).Max(120).Int()) + .Field("email", Z.String().Email()) + .Build(); +``` + +## Source generator + +Mark a class, struct, or record with `[ZodSchema]` and a zero-allocation validator is generated at compile time: + +```csharp +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 on failure + +// Value-first composition methods (enable via EnableComposition, on by default): +var adult = UserSchema.ApplyRefine(user, u => u.Age >= 18, "Must be adult"); +``` + +DataAnnotations attributes such as `[Required]`, `[Length]`, `[StringLength]`, `[MinLength]`, `[MaxLength]`, `[Range]`, `[RegularExpression]`, `[AllowedValues]`, `[DeniedValues]`, `[EmailAddress]`, and `[Compare]` are validated with direct, typed codegen (no reflection). + +## JSON Schema export + +```csharp +var jsonSchema = Z.ToJsonSchema(userSchema, new ToJsonSchemaOptions { Title = "User" }); +``` + +## Further reading + +For the complete API surface, performance notes, and cross-platform TypeScript/Zod interop see the [repository README](https://github.com/purview-dev/zodsharp/blob/main/README.md). + +## License + +MIT — see the package metadata in `Purview.ZodSharp` on NuGet. diff --git a/src/src/ZodSharp/ZodSharp.csproj b/src/src/ZodSharp/ZodSharp.csproj index 5023749..2485906 100644 --- a/src/src/ZodSharp/ZodSharp.csproj +++ b/src/src/ZodSharp/ZodSharp.csproj @@ -22,10 +22,6 @@ /> - - - - diff --git a/src/tests/AspNetCore.UnitTests/AssemblyAttributes.cs b/src/tests/AspNetCore.UnitTests/Properties/AssemblyInfo.cs similarity index 100% rename from src/tests/AspNetCore.UnitTests/AssemblyAttributes.cs rename to src/tests/AspNetCore.UnitTests/Properties/AssemblyInfo.cs diff --git a/src/tests/SourceGenerators.UnitTests/Infra/ZodAnalyzerTestOptions.cs b/src/tests/SourceGenerators.UnitTests/Infra/ZodAnalyzerTestOptions.cs index c03d12a..e20384a 100644 --- a/src/tests/SourceGenerators.UnitTests/Infra/ZodAnalyzerTestOptions.cs +++ b/src/tests/SourceGenerators.UnitTests/Infra/ZodAnalyzerTestOptions.cs @@ -30,7 +30,7 @@ public sealed class ZodSchemaAttribute : System.Attribute public string? SchemaName { get; init; } public bool GenerateValidateMethod { get; init; } = true; public bool GenerateParseMethod { get; init; } = true; - public bool EnableComposition { get; init; } = false; + public bool EnableComposition { get; init; } = true; public string? CustomValidationMethodName { get; init; } public string? RefinementMethodName { get; init; } public bool GenerateIValidateOptions { get; init; } = false; diff --git a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.CompositionMethods.cs b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.CompositionMethods.cs index 5605296..b316cab 100644 --- a/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.CompositionMethods.cs +++ b/src/tests/SourceGenerators.UnitTests/ZodSchemaGeneratorTests.CompositionMethods.cs @@ -29,6 +29,32 @@ public class ComposeModel await Assert.That(generatedSource).ContainsGeneratedCode("ApplyRefine"); } + [Test] + public async Task CompositionMethods_GivenDisabledComposition_AreNotGenerated(CancellationToken cancellationToken) + { + // Arrange + const string source = + @" +namespace Testing +{ + [ZodSchema(EnableComposition = false)] + public class NoComposeModel + { + public string? Name { get; set; } + } +} +"; + + // Act + var driverResult = await GenerateAsync(source, cancellationToken); + var generatedSource = driverResult.GetSource("NoComposeModelSchema"); + + // Assert + await Assert.That(generatedSource).DoesNotContain("ApplyAnd"); + await Assert.That(generatedSource).DoesNotContain("ApplyOr"); + await Assert.That(generatedSource).DoesNotContain("ApplyRefine"); + } + [Test] public async Task CompositionMethods_GivenValidValue_ApplyRefineSucceeds(CancellationToken cancellationToken) { diff --git a/src/tests/ZodSharp.Benchmarks/README.md b/src/tests/ZodSharp.Benchmarks/README.md deleted file mode 100644 index ac40472..0000000 --- a/src/tests/ZodSharp.Benchmarks/README.md +++ /dev/null @@ -1,118 +0,0 @@ -# ZodSharp Performance Tests - -This folder contains comprehensive performance tests for the ZodSharp library using BenchmarkDotNet. - -## Structure - -- **BasicPerformanceTests.cs**: Basic validation tests (strings, numbers, simple arrays) -- **ObjectPerformanceTests.cs**: Object validation tests (simple, medium, complex) -- **ArrayPerformanceTests.cs**: Tests with arrays of different sizes -- **HeavyPerformanceTests.cs**: Heavy tests (deeply nested objects, many fields, refinements) -- **MemoryPerformanceTests.cs**: Tests focused on memory allocations -- **TransformPerformanceTests.cs**: Transformation tests (ToLower, ToUpper, Trim) -- **UnionPerformanceTests.cs**: Union and discriminated union tests - -## How to Run - -### Build and Run - -```bash -# Build the solution -dotnet build src/ZodSharp.slnx - -# Run all benchmarks -dotnet run --project src/tests/ZodSharp.Benchmarks/ZodSharp.Benchmarks.csproj -c Release -``` - -### Run Specific Benchmarks - -```bash -# Run only basic tests -dotnet run --project src/tests/ZodSharp.Benchmarks/ZodSharp.Benchmarks.csproj -c Release --filter "*BasicPerformanceTests*" - -# Run only memory tests -dotnet run --project src/tests/ZodSharp.Benchmarks/ZodSharp.Benchmarks.csproj -c Release --filter "*MemoryPerformanceTests*" -``` - -## Results - -Benchmark results are saved in the `BenchmarkDotNet.Artifacts` folder after execution. You will find: - -- **HTML Reports**: Detailed visualization of results -- **Markdown Reports**: Summary of results -- **Logs**: Detailed execution logs - -## Test Cases - -### Basic Cases -- Simple string validation -- Number validation -- Small array validation -- Validation with multiple rules - -### Medium Cases -- Objects with 5-10 fields -- Arrays with 100 elements -- Validations with multiple chained rules - -### Heavy Cases -- Deeply nested objects (4+ levels) -- Objects with 50+ fields -- Arrays with 1000+ elements -- Multiple chained refinements -- Large objects with nested arrays - -### Memory Tests -- Allocations per validation -- Multiple iterations to identify leaks -- Comparison of allocations between different validation types - -## Interpreting Results - -### Important Metrics - -1. **Mean**: Average execution time -2. **Error**: Error margin -3. **StdDev**: Standard deviation -4. **Gen 0/1/2**: Garbage collections per generation -5. **Allocated**: Memory allocated per operation - -### Identifying Performance Issues - -- **High time**: Check for unnecessary operations or inefficient loops -- **High allocations**: Look for unnecessary allocations, especially in hot paths -- **Gen 2 collections**: Indicates long-lived objects that can be optimized - -## Optimization Tips - -1. Run benchmarks in Release mode for more accurate results -2. Run multiple times to ensure consistency -3. Compare results before and after optimizations -4. Use MemoryDiagnoser to identify unnecessary allocations -5. Focus on optimizing the most common cases first - -## Adding New Tests - -To add new performance tests: - -1. Create a new class inheriting from `BenchmarkDotNet.Attributes` -2. Add the `[MemoryDiagnoser]` attribute for memory analysis -3. Add the `[SimpleJob]` attribute to configure the benchmark -4. Create methods marked with `[Benchmark]` -5. Run and analyze the results - -Example: - -```csharp -[MemoryDiagnoser] -[SimpleJob(launchCount: 1, warmupCount: 3, iterationCount: 5)] -public class MyPerformanceTests -{ - [Benchmark] - public ValidationResult MyTest() - { - // Seu código de teste aqui - } -} -``` - diff --git a/src/tests/ZodSharp.UnitTests/Core/AssemblyAttributes.cs b/src/tests/ZodSharp.UnitTests/Properties/AssemblyInfo.cs similarity index 100% rename from src/tests/ZodSharp.UnitTests/Core/AssemblyAttributes.cs rename to src/tests/ZodSharp.UnitTests/Properties/AssemblyInfo.cs From df652712d98bc3348c8230e65d1f2cb2bd380cef Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Mon, 14 Sep 2026 16:00:08 +0100 Subject: [PATCH 2/4] chore: disabled packable state for benchmarks project --- README.md | 4 +- src/src/Benchmarks/Benchmarks.csproj | 1 + src/src/Benchmarks/README.md | 101 ++++++++++++++++++++++++++- 3 files changed, 101 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 639facb..daf09b6 100644 --- a/README.md +++ b/README.md @@ -275,8 +275,8 @@ We maintain comprehensive performance tests in `src/src/Benchmarks`. Run them yo # Run all performance benchmarks dotnet run --project src/src/Benchmarks/Benchmarks.csproj -c Release -# Run specific test suites -dotnet run --project src/src/Benchmarks/Benchmarks.csproj -c Release --filter "*MemoryPerformanceTests*" +# Run specific test suites (the `--` passes the filter to BenchmarkDotNet) +dotnet run --project src/src/Benchmarks/Benchmarks.csproj -c Release -- --filter "*MemoryPerformanceTests*" ``` **Key performance highlights**: diff --git a/src/src/Benchmarks/Benchmarks.csproj b/src/src/Benchmarks/Benchmarks.csproj index b01a802..f96631e 100644 --- a/src/src/Benchmarks/Benchmarks.csproj +++ b/src/src/Benchmarks/Benchmarks.csproj @@ -6,6 +6,7 @@ ZodSharp $(NoWarn);CA1303;CA1707;CA1822;CA1515; Exe + false Benchmarks diff --git a/src/src/Benchmarks/README.md b/src/src/Benchmarks/README.md index 4b3c38f..2a490d2 100644 --- a/src/src/Benchmarks/README.md +++ b/src/src/Benchmarks/README.md @@ -26,11 +26,11 @@ dotnet run --project src/src/Benchmarks/Benchmarks.csproj -c Release just perf-tests ``` -Run a single suite with `--filter`: +Run a single suite with `--filter` (note the `--` separator so the filter reaches BenchmarkDotNet, not `dotnet run`): ```bash -dotnet run --project src/src/Benchmarks/Benchmarks.csproj -c Release --filter "*BasicPerformanceTests*" -dotnet run --project src/src/Benchmarks/Benchmarks.csproj -c Release --filter "*MemoryPerformanceTests*" +dotnet run --project src/src/Benchmarks/Benchmarks.csproj -c Release -- --filter "*BasicPerformanceTests*" +dotnet run --project src/src/Benchmarks/Benchmarks.csproj -c Release -- --filter "*MemoryPerformanceTests*" ``` Always use `-c Release`; running benchmarks in Debug produces meaningless numbers. @@ -43,6 +43,101 @@ Benchmark results are written to the `BenchmarkDotNet.Artifacts` folder in the p - **Markdown reports** — summary tables suitable for commit/PR comments - **Logs** — detailed execution logs +The tables below were captured with the committed suite on the following environment (release build, .NET 10): + +- BenchmarkDotNet v0.15.8, Windows 11 (10.0.28020.2991) +- 13th Gen Intel Core i9-13900KF 3.00 GHz, 1 CPU, 32 logical / 24 physical cores +- .NET SDK 10.0.401, runtime .NET 10.0.12, X64 RyuJIT x86-64-v3 + +### Basic + +`IterationCount=5 LaunchCount=1 WarmupCount=3` + +| Method | Mean | Error | StdDev | Gen0 | Allocated | +|-------------------------------- |---------------:|---------------:|--------------:|-------:|----------:| +| ValidateString | 42.746 ns | 1.8166 ns | 0.4718 ns | - | - | +| ValidateNumber | 11.585 ns | 0.0628 ns | 0.0163 ns | - | - | +| ValidateBoolean | 2.904 ns | 0.0372 ns | 0.0097 ns | - | - | +| ValidateStringArray | 91.207 ns | 2.8108 ns | 0.7300 ns | 0.0318 | 600 B | +| ValidateStringWithMultipleRules | 970,250.195 ns | 25,651.4777 ns | 6,661.6073 ns | - | 15048 B | +| ValidateNumberWithMultipleRules | 57.008 ns | 1.4734 ns | 0.3826 ns | 0.0166 | 312 B | + +### Object + +`IterationCount=5 LaunchCount=1 WarmupCount=3` + +| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +|----------------------------- |-----------:|---------:|---------:|-------:|-------:|----------:| +| ValidateSimpleObject | 136.3 ns | 2.62 ns | 0.68 ns | 0.0224 | - | 424 B | +| ValidateMediumObject | 454.2 ns | 16.88 ns | 2.61 ns | 0.0672 | - | 1272 B | +| ValidateComplexObject | 1,067.0 ns | 65.47 ns | 17.00 ns | 0.1545 | - | 2936 B | +| ValidateComplexObjectInvalid | 1,386.3 ns | 70.29 ns | 18.25 ns | 0.2422 | 0.0019 | 4560 B | + +### Array + +`IterationCount=5 LaunchCount=1 WarmupCount=3` + +| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +|------------------------------------ |-------------:|-------------:|------------:|--------:|--------:|----------:| +| ValidateSmallArray | 175.9 ns | 4.13 ns | 1.07 ns | 0.0496 | - | 936 B | +| ValidateMediumArray | 5,087.2 ns | 34.46 ns | 8.95 ns | 0.4272 | 0.0076 | 8136 B | +| ValidateLargeArray | 14,841.8 ns | 194.64 ns | 50.55 ns | 4.2419 | 1.0529 | 80136 B | +| ValidateNumberArray | 152,157.4 ns | 3,853.20 ns | 596.29 ns | 32.4707 | 30.2734 | 612136 B | +| ValidateLargeArrayWithComplexSchema | 974,079.3 ns | 24,780.85 ns | 3,834.86 ns | - | - | 23248 B | +| ValidateLargeArrayInvalid | 14,911.2 ns | 636.74 ns | 98.54 ns | 4.2877 | 1.0681 | 80888 B | + +### Heavy + +`IterationCount=3 LaunchCount=1 WarmupCount=2` + +| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +|---------------------------------- |-------------:|-------------:|------------:|-------:|-------:|----------:| +| ValidateDeepNestedObject | 337.3 ns | 27.29 ns | 1.50 ns | 0.0710 | 0.0005 | 1.31 KB | +| ValidateWideObject | 2,726.7 ns | 221.74 ns | 12.15 ns | 0.2670 | 0.0038 | 4.92 KB | +| ValidateNestedArray | 321.5 ns | 75.48 ns | 4.14 ns | 0.1040 | - | 1.91 KB | +| ValidateStringWithManyRefinements | 987,030.7 ns | 67,218.17 ns | 3,684.45 ns | - | - | 15.13 KB | +| ValidateLargeObjectWithArrays | 38,199.8 ns | 9,842.49 ns | 539.50 ns | 6.8970 | 2.0752 | 127.64 KB | + +### Transform + +`IterationCount=5 LaunchCount=1 WarmupCount=3` + +| Method | Mean | Error | StdDev | Gen0 | Allocated | +|------------------------ |----------:|---------:|---------:|-------:|----------:| +| TransformToLower | 22.24 ns | 0.544 ns | 0.141 ns | 0.0025 | 48 B | +| TransformToUpper | 23.18 ns | 0.940 ns | 0.244 ns | 0.0025 | 48 B | +| TransformTrim | 31.34 ns | 1.297 ns | 0.337 ns | 0.0025 | 48 B | +| TransformChained | 40.45 ns | 1.096 ns | 0.170 ns | 0.0051 | 96 B | +| TransformWithValidation | 144.83 ns | 5.236 ns | 1.360 ns | 0.0288 | 544 B | + +### Union + +`IterationCount=5 LaunchCount=1 WarmupCount=3` + +| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +|---------------------------------------- |----------:|----------:|---------:|-------:|-------:|----------:| +| ValidateUnion_String | 57.70 ns | 0.552 ns | 0.085 ns | 0.0033 | - | 64 B | +| ValidateUnion_Number | 72.18 ns | 2.130 ns | 0.330 ns | 0.0314 | - | 592 B | +| ValidateUnion_Boolean | 112.88 ns | 2.550 ns | 0.662 ns | 0.0421 | - | 792 B | +| ValidateDiscriminatedUnion_FirstOption | 233.45 ns | 6.170 ns | 1.602 ns | 0.0246 | - | 464 B | +| ValidateDiscriminatedUnion_SecondOption | 259.09 ns | 13.888 ns | 2.149 ns | 0.0443 | - | 840 B | +| ValidateUnion_Invalid | 201.83 ns | 4.354 ns | 1.131 ns | 0.0722 | 0.0002 | 1360 B | + +### Memory + +`IterationCount=10 LaunchCount=1 WarmupCount=3` + +| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | +|------------------------------ |--------------:|-------------:|-------------:|---------:|--------:|--------:|-------:|----------:|------------:| +| ValidateString_Allocations | 42.31 ns | 0.774 ns | 0.512 ns | 1.00 | 0.02 | - | - | - | NA | +| ValidateArray_Allocations | 1,512.42 ns | 33.074 ns | 19.682 ns | 35.75 | 0.61 | 0.4807 | 0.0019 | 9048 B | NA | +| ValidateObject_Allocations | 145.85 ns | 1.476 ns | 0.878 ns | 3.45 | 0.05 | 0.0350 | - | 664 B | NA | +| ValidateString_ManyIterations | 42,098.80 ns | 239.443 ns | 142.488 ns | 995.08 | 12.12 | - | - | - | NA | +| ValidateArray_ManyIterations | 147,362.19 ns | 4,961.440 ns | 3,281.686 ns | 3,483.18 | 84.52 | 43.2129 | 1.2207 | 814512 B | NA | +| ValidateObject_ManyIterations | 135,243.05 ns | 1,037.450 ns | 686.209 ns | 3,196.72 | 40.59 | 22.4609 | - | 424240 B | NA | + +> `Gen0`/`Gen1` are GC collections per 1000 operations; `Allocated` is managed memory per operation. "NA" allocations mean zero bytes were measured. + ## Interpreting results Key metrics reported per benchmark: From bc77cdca2891e792f8a43d38a2427cf33a8b5344 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Mon, 14 Sep 2026 16:01:20 +0100 Subject: [PATCH 3/4] docs: updated perf docs --- README.md | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index daf09b6..f7cf4d3 100644 --- a/README.md +++ b/README.md @@ -208,13 +208,19 @@ ZodSharp is designed for maximum performance with zero-allocation validation and ### 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 +**Typical validation times** (measured with the committed benchmark suite on .NET 10.0, Release mode, 13th Gen Intel Core i9-13900KF): + +- Boolean validation: **~3 ns** per validation (zero allocation) +- Number validation: **~12 ns** per validation (zero allocation) +- Simple string validation: **~43 ns** per validation (zero allocation) +- Small string array (5 items): **~91 ns** per validation +- Simple object (2 fields): **~136 ns** per validation +- Deeply nested object (4 levels): **~337 ns** per validation +- Medium object (6 fields): **~454 ns** per validation +- Complex object (13 fields with nesting): **~1.1 μs** per validation +- Wide object (50 fields): **~2.7 μs** per validation +- Medium array (100 items): **~5.1 μs** per validation +- Large array (1000 items): **~14.8 μs** per validation **Memory efficiency**: @@ -223,6 +229,8 @@ ZodSharp is designed for maximum performance with zero-allocation validation and - Struct-based rules avoid GC pressure - No reflection overhead in hot paths +Full results for every suite are in the [performance README](src/src/Benchmarks/README.md). + ### Performance Optimizations ZodSharp implements several optimizations for maximum performance: From ba38f08e5c45721a170cff559dd9fa76c7fe6038 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Mon, 14 Sep 2026 16:49:58 +0100 Subject: [PATCH 4/4] performance: allocation fixes --- .github/workflows/release.yml | 2 +- README.md | 26 +-- src/src/Benchmarks/ArrayPerformanceTests.cs | 23 +-- src/src/Benchmarks/BasicPerformanceTests.cs | 25 +-- src/src/Benchmarks/HeavyPerformanceTests.cs | 98 +++++----- src/src/Benchmarks/MemoryPerformanceTests.cs | 25 ++- src/src/Benchmarks/README.md | 106 ++++++----- .../Benchmarks/TransformPerformanceTests.cs | 9 +- .../ZodSharp/Schemas/FieldSchemaWrapper.cs | 22 +-- .../ZodSharp/Schemas/SchemaValueCoercion.cs | 52 +++++ src/src/ZodSharp/Schemas/ZodArray.cs | 74 +++++--- src/src/ZodSharp/Schemas/ZodObject.cs | 178 ++++++++++++------ src/src/ZodSharp/Schemas/ZodObjectBuilder.cs | 22 +-- src/src/ZodSharp/Schemas/ZodUnion.cs | 5 +- 14 files changed, 381 insertions(+), 286 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5f4c7eb..162c17e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false + cancel-in-progress: true jobs: release: diff --git a/README.md b/README.md index f7cf4d3..e04ca40 100644 --- a/README.md +++ b/README.md @@ -210,24 +210,24 @@ ZodSharp is designed for maximum performance with zero-allocation validation and **Typical validation times** (measured with the committed benchmark suite on .NET 10.0, Release mode, 13th Gen Intel Core i9-13900KF): -- Boolean validation: **~3 ns** per validation (zero allocation) -- Number validation: **~12 ns** per validation (zero allocation) -- Simple string validation: **~43 ns** per validation (zero allocation) -- Small string array (5 items): **~91 ns** per validation -- Simple object (2 fields): **~136 ns** per validation -- Deeply nested object (4 levels): **~337 ns** per validation -- Medium object (6 fields): **~454 ns** per validation -- Complex object (13 fields with nesting): **~1.1 μs** per validation -- Wide object (50 fields): **~2.7 μs** per validation -- Medium array (100 items): **~5.1 μs** per validation -- Large array (1000 items): **~14.8 μs** per validation +- Boolean validation: **~2 ns** per validation (zero allocation) +- Number validation: **~11 ns** per validation (zero allocation) +- Simple string validation: **~42 ns** per validation (zero allocation) +- Small string array (5 items): **~57 ns** per validation (zero allocation) +- Simple object (2 fields): **~90 ns** per validation (zero allocation) +- Deeply nested object (4 levels): **~199 ns** per validation (zero allocation) +- Medium object (6 fields): **~340 ns** per validation (zero allocation) +- Complex object (13 fields with nesting): **~796 ns** per validation (zero allocation) +- Wide object (50 fields): **~2.2 μs** per validation (zero allocation) +- Medium array (100 items): **~4.6 μs** per validation (zero allocation) +- Large array (1000 items): **~11.7 μs** per validation (zero allocation) **Memory efficiency**: -- Zero allocations for simple validations (strings, numbers, booleans) -- Minimal allocations for arrays and objects (only for error collections) +- **Zero allocations for every valid-input path** — primitives, strings, arrays, objects, unions, and discriminated unions all validate without allocating when the input is valid - Struct-based rules avoid GC pressure - No reflection overhead in hot paths +- Allocations only occur on the failure path (error collection) and inside string transforms (`ToLower`/`ToUpper`/`Trim` produce a new string) Full results for every suite are in the [performance README](src/src/Benchmarks/README.md). diff --git a/src/src/Benchmarks/ArrayPerformanceTests.cs b/src/src/Benchmarks/ArrayPerformanceTests.cs index dc5fe51..6624a7d 100644 --- a/src/src/Benchmarks/ArrayPerformanceTests.cs +++ b/src/src/Benchmarks/ArrayPerformanceTests.cs @@ -15,10 +15,12 @@ public class ArrayPerformanceTests readonly ZodArray _mediumArraySchema; readonly ZodArray _largeArraySchema; readonly ZodArray _numberArraySchema; + readonly ZodArray _complexSchemaArraySchema; readonly string[] _smallArray; readonly string[] _mediumArray; readonly string[] _largeArray; + readonly string[] _largeArrayInvalid; readonly double[] _numberArray; public ArrayPerformanceTests() @@ -26,11 +28,15 @@ public ArrayPerformanceTests() _smallArraySchema = Z.Array(Z.String().Min(1).Max(10)).Min(1).Max(10); _mediumArraySchema = Z.Array(Z.String().Email()).Min(1).Max(100); _largeArraySchema = Z.Array(Z.String().Min(1).Max(50)).Min(1).Max(1000); - _numberArraySchema = Z.Array(Z.Number().Min(0).Max(100).Int()).Min(1).Max(1000); + _numberArraySchema = Z.Array(Z.Number().Min(0).Max(1000).Int()).Min(1).Max(1000); + _complexSchemaArraySchema = Z.Array( + Z.String().Min(5).Max(100).Email().Regex(@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$") + ); _smallArray = [.. Enumerable.Range(1, 10).Select(static i => $"item{i}")]; _mediumArray = [.. Enumerable.Range(1, 100).Select(static i => $"user{i}@example.com")]; _largeArray = [.. Enumerable.Range(1, 1000).Select(static i => $"item{i}")]; + _largeArrayInvalid = [.. _largeArray, ""]; _numberArray = [.. Enumerable.Range(1, 1000).Select(static i => (double)i)]; } @@ -47,18 +53,9 @@ public ArrayPerformanceTests() public ValidationResult ValidateNumberArray() => _numberArraySchema.Validate(_numberArray); [Benchmark] - public ValidationResult ValidateLargeArrayWithComplexSchema() - { - var schema = Z.Array( - Z.String().Min(5).Max(100).Email().Regex(@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$") - ); - return schema.Validate(_mediumArray); - } + public ValidationResult ValidateLargeArrayWithComplexSchema() => + _complexSchemaArraySchema.Validate(_mediumArray); [Benchmark] - public ValidationResult ValidateLargeArrayInvalid() - { - var invalid = _largeArray.Concat([""]).ToArray(); - return _largeArraySchema.Validate(invalid); - } + public ValidationResult ValidateLargeArrayInvalid() => _largeArraySchema.Validate(_largeArrayInvalid); } diff --git a/src/src/Benchmarks/BasicPerformanceTests.cs b/src/src/Benchmarks/BasicPerformanceTests.cs index e124ba8..57789c3 100644 --- a/src/src/Benchmarks/BasicPerformanceTests.cs +++ b/src/src/Benchmarks/BasicPerformanceTests.cs @@ -15,6 +15,9 @@ public class BasicPerformanceTests readonly ZodNumber _numberSchema; readonly ZodBoolean _booleanSchema; readonly ZodArray _arraySchema; + readonly ZodString _multiRuleStringSchema; + readonly ZodNumber _multiRuleNumberSchema; + readonly string[] _stringArray; public BasicPerformanceTests() { @@ -22,6 +25,13 @@ public BasicPerformanceTests() _numberSchema = Z.Number().Min(0).Max(100).Int(); _booleanSchema = Z.Boolean(); _arraySchema = Z.Array(Z.String()).Min(1).Max(10); + _multiRuleStringSchema = Z.String() + .Min(5) + .Max(100) + .Email() + .Regex(@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"); + _multiRuleNumberSchema = Z.Number().Min(0).Max(100).Int().MultipleOf(2); + _stringArray = ["a", "b", "c", "d", "e"]; } [Benchmark] @@ -34,19 +44,12 @@ public BasicPerformanceTests() public ValidationResult ValidateBoolean() => _booleanSchema.Validate(true); [Benchmark] - public ValidationResult ValidateStringArray() => _arraySchema.Validate(["a", "b", "c", "d", "e"]); + public ValidationResult ValidateStringArray() => _arraySchema.Validate(_stringArray); [Benchmark] - public ValidationResult ValidateStringWithMultipleRules() - { - var schema = Z.String().Min(5).Max(100).Email().Regex(@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"); - return schema.Validate("test@example.com"); - } + public ValidationResult ValidateStringWithMultipleRules() => + _multiRuleStringSchema.Validate("test@example.com"); [Benchmark] - public ValidationResult ValidateNumberWithMultipleRules() - { - var schema = Z.Number().Min(0).Max(100).Int().MultipleOf(2); - return schema.Validate(42.0); - } + public ValidationResult ValidateNumberWithMultipleRules() => _multiRuleNumberSchema.Validate(42.0); } diff --git a/src/src/Benchmarks/HeavyPerformanceTests.cs b/src/src/Benchmarks/HeavyPerformanceTests.cs index b549985..0280ba3 100644 --- a/src/src/Benchmarks/HeavyPerformanceTests.cs +++ b/src/src/Benchmarks/HeavyPerformanceTests.cs @@ -14,10 +14,13 @@ public class HeavyPerformanceTests readonly ZodObject _deepNestedSchema; readonly ZodObject _wideObjectSchema; readonly ZodArray _nestedArraySchema; + readonly ZodRefinement _refinementStringSchema; + readonly ZodObject _largeObjectWithArraysSchema; readonly Dictionary _deepNestedObject; readonly Dictionary _wideObject; readonly string[][] _nestedArray; + readonly Dictionary _largeObjectWithArrays; static readonly string[] TagTestingValues = ["tag1", "tag2", "tag3"]; @@ -43,6 +46,48 @@ public HeavyPerformanceTests() var nestedArray = Z.Array(stringArray); _nestedArraySchema = nestedArray; + _refinementStringSchema = Z.String() + .Min(5) + .Max(100) + .Email() + .Regex(@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$") + .StartsWith("user") + .EndsWith(".com") + .Refine(static s => s.Contains('@', StringComparison.Ordinal), "Must contain @") + .Refine(static s => s.Length > 10, "Must be longer than 10") + .Refine(static s => s.Count(static c => c == '.') <= 2, "Too many dots"); + + _largeObjectWithArraysSchema = Z.Object() + .Field( + "items", + Z.Array( + Z.Object() + .Field("id", Z.String().UUID()) + .Field("name", Z.String().Min(1).Max(100)) + .Field("tags", Z.Array(Z.String()).Min(0).Max(10)) + .Build() + ) + .Min(1) + .Max(100) + ) + .Build(); + + _largeObjectWithArrays = new() + { + { + "items", + Enumerable + .Range(1, 100) + .Select(static i => new Dictionary + { + { "id", Guid.NewGuid().ToString() }, + { "name", $"Item {i}" }, + { "tags", TagTestingValues }, + }) + .ToArray() + }, + }; + _deepNestedObject = new Dictionary { { @@ -90,55 +135,10 @@ public HeavyPerformanceTests() public ValidationResult ValidateNestedArray() => _nestedArraySchema.Validate(_nestedArray); [Benchmark] - public ValidationResult ValidateStringWithManyRefinements() - { - var schema = Z.String() - .Min(5) - .Max(100) - .Email() - .Regex(@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$") - .StartsWith("user") - .EndsWith(".com") - .Refine(static s => s.Contains('@', StringComparison.Ordinal), "Must contain @") - .Refine(static s => s.Length > 10, "Must be longer than 10") - .Refine(static s => s.Count(static c => c == '.') <= 2, "Too many dots"); - return schema.Validate("user@example.com"); - } + public ValidationResult ValidateStringWithManyRefinements() => + _refinementStringSchema.Validate("user@example.com"); [Benchmark] - public ValidationResult> ValidateLargeObjectWithArrays() - { - var schema = Z.Object() - .Field( - "items", - Z.Array( - Z.Object() - .Field("id", Z.String().UUID()) - .Field("name", Z.String().Min(1).Max(100)) - .Field("tags", Z.Array(Z.String()).Min(0).Max(10)) - .Build() - ) - .Min(1) - .Max(100) - ) - .Build(); - - Dictionary data = new() - { - { - "items", - Enumerable - .Range(1, 100) - .Select(static i => new Dictionary - { - { "id", Guid.NewGuid().ToString() }, - { "name", $"Item {i}" }, - { "tags", TagTestingValues }, - }) - .ToArray() - }, - }; - - return schema.Validate(data); - } + public ValidationResult> ValidateLargeObjectWithArrays() => + _largeObjectWithArraysSchema.Validate(_largeObjectWithArrays); } diff --git a/src/src/Benchmarks/MemoryPerformanceTests.cs b/src/src/Benchmarks/MemoryPerformanceTests.cs index c5c2a22..d073d90 100644 --- a/src/src/Benchmarks/MemoryPerformanceTests.cs +++ b/src/src/Benchmarks/MemoryPerformanceTests.cs @@ -15,29 +15,28 @@ public class MemoryPerformanceTests readonly ZodArray _arraySchema; readonly ZodObject _objectSchema; + readonly double[] _arrayData; + readonly Dictionary _objectData; + public MemoryPerformanceTests() { _stringSchema = Z.String().Min(1).Max(100).Email(); _arraySchema = Z.Array(Z.Number().Min(0).Max(100)).Min(1).Max(100); _objectSchema = Z.Object().Field("name", Z.String().Min(1)).Field("age", Z.Number().Min(0)).Build(); + + _arrayData = [.. Enumerable.Range(1, 100).Select(static i => (double)i)]; + _objectData = new() { { "name", "John" }, { "age", 30.0 } }; } [Benchmark(Baseline = true)] public ValidationResult ValidateString_Allocations() => _stringSchema.Validate("user@example.com"); [Benchmark] - public ValidationResult ValidateArray_Allocations() - { - var data = Enumerable.Range(1, 100).Select(static i => (double)i).ToArray(); - return _arraySchema.Validate(data); - } + public ValidationResult ValidateArray_Allocations() => _arraySchema.Validate(_arrayData); [Benchmark] - public ValidationResult> ValidateObject_Allocations() - { - Dictionary data = new() { { "name", "John" }, { "age", 30.0 } }; - return _objectSchema.Validate(data); - } + public ValidationResult> ValidateObject_Allocations() => + _objectSchema.Validate(_objectData); [Benchmark] public void ValidateString_ManyIterations() @@ -51,20 +50,18 @@ public void ValidateString_ManyIterations() [Benchmark] public void ValidateArray_ManyIterations() { - var data = Enumerable.Range(1, 100).Select(static i => (double)i).ToArray(); for (var i = 0; i < 100; i++) { - _arraySchema.Validate(data); + _arraySchema.Validate(_arrayData); } } [Benchmark] public void ValidateObject_ManyIterations() { - Dictionary data = new() { { "name", "John" }, { "age", 30.0 } }; for (var i = 0; i < 1000; i++) { - _objectSchema.Validate(data); + _objectSchema.Validate(_objectData); } } } diff --git a/src/src/Benchmarks/README.md b/src/src/Benchmarks/README.md index 2a490d2..84669fe 100644 --- a/src/src/Benchmarks/README.md +++ b/src/src/Benchmarks/README.md @@ -53,90 +53,92 @@ The tables below were captured with the committed suite on the following environ `IterationCount=5 LaunchCount=1 WarmupCount=3` -| Method | Mean | Error | StdDev | Gen0 | Allocated | -|-------------------------------- |---------------:|---------------:|--------------:|-------:|----------:| -| ValidateString | 42.746 ns | 1.8166 ns | 0.4718 ns | - | - | -| ValidateNumber | 11.585 ns | 0.0628 ns | 0.0163 ns | - | - | -| ValidateBoolean | 2.904 ns | 0.0372 ns | 0.0097 ns | - | - | -| ValidateStringArray | 91.207 ns | 2.8108 ns | 0.7300 ns | 0.0318 | 600 B | -| ValidateStringWithMultipleRules | 970,250.195 ns | 25,651.4777 ns | 6,661.6073 ns | - | 15048 B | -| ValidateNumberWithMultipleRules | 57.008 ns | 1.4734 ns | 0.3826 ns | 0.0166 | 312 B | +| Method | Mean | Error | StdDev | Allocated | +|-------------------------------- |----------:|----------:|----------:|----------:| +| ValidateString | 41.627 ns | 0.8568 ns | 0.2225 ns | - | +| ValidateNumber | 10.702 ns | 0.2730 ns | 0.0709 ns | - | +| ValidateBoolean | 2.170 ns | 0.0314 ns | 0.0082 ns | - | +| ValidateStringArray | 57.031 ns | 2.6305 ns | 0.6831 ns | - | +| ValidateStringWithMultipleRules | 77.313 ns | 0.4016 ns | 0.1043 ns | - | +| ValidateNumberWithMultipleRules | 16.427 ns | 0.4199 ns | 0.1090 ns | - | ### Object `IterationCount=5 LaunchCount=1 WarmupCount=3` -| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | -|----------------------------- |-----------:|---------:|---------:|-------:|-------:|----------:| -| ValidateSimpleObject | 136.3 ns | 2.62 ns | 0.68 ns | 0.0224 | - | 424 B | -| ValidateMediumObject | 454.2 ns | 16.88 ns | 2.61 ns | 0.0672 | - | 1272 B | -| ValidateComplexObject | 1,067.0 ns | 65.47 ns | 17.00 ns | 0.1545 | - | 2936 B | -| ValidateComplexObjectInvalid | 1,386.3 ns | 70.29 ns | 18.25 ns | 0.2422 | 0.0019 | 4560 B | +| Method | Mean | Error | StdDev | Gen0 | Allocated | +|----------------------------- |------------:|----------:|----------:|-------:|----------:| +| ValidateSimpleObject | 89.78 ns | 2.797 ns | 0.726 ns | - | - | +| ValidateMediumObject | 340.18 ns | 12.943 ns | 3.361 ns | - | - | +| ValidateComplexObject | 795.69 ns | 51.004 ns | 13.246 ns | - | - | +| ValidateComplexObjectInvalid | 1,152.80 ns | 35.102 ns | 9.116 ns | 0.1030 | 1960 B | ### Array `IterationCount=5 LaunchCount=1 WarmupCount=3` -| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | -|------------------------------------ |-------------:|-------------:|------------:|--------:|--------:|----------:| -| ValidateSmallArray | 175.9 ns | 4.13 ns | 1.07 ns | 0.0496 | - | 936 B | -| ValidateMediumArray | 5,087.2 ns | 34.46 ns | 8.95 ns | 0.4272 | 0.0076 | 8136 B | -| ValidateLargeArray | 14,841.8 ns | 194.64 ns | 50.55 ns | 4.2419 | 1.0529 | 80136 B | -| ValidateNumberArray | 152,157.4 ns | 3,853.20 ns | 596.29 ns | 32.4707 | 30.2734 | 612136 B | -| ValidateLargeArrayWithComplexSchema | 974,079.3 ns | 24,780.85 ns | 3,834.86 ns | - | - | 23248 B | -| ValidateLargeArrayInvalid | 14,911.2 ns | 636.74 ns | 98.54 ns | 4.2877 | 1.0681 | 80888 B | +| Method | Mean | Error | StdDev | Gen0 | Allocated | +|------------------------------------ |------------:|----------:|----------:|-------:|----------:| +| ValidateSmallArray | 124.4 ns | 1.38 ns | 0.36 ns | - | - | +| ValidateMediumArray | 4,585.3 ns | 313.30 ns | 81.36 ns | - | - | +| ValidateLargeArray | 11,682.3 ns | 913.59 ns | 237.26 ns | - | - | +| ValidateNumberArray | 12,797.3 ns | 716.73 ns | 110.91 ns | - | - | +| ValidateLargeArrayWithComplexSchema | 8,011.8 ns | 531.73 ns | 138.09 ns | - | - | +| ValidateLargeArrayInvalid | 11,206.7 ns | 379.31 ns | 98.51 ns | 0.0458 | 904 B | ### Heavy `IterationCount=3 LaunchCount=1 WarmupCount=2` -| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | -|---------------------------------- |-------------:|-------------:|------------:|-------:|-------:|----------:| -| ValidateDeepNestedObject | 337.3 ns | 27.29 ns | 1.50 ns | 0.0710 | 0.0005 | 1.31 KB | -| ValidateWideObject | 2,726.7 ns | 221.74 ns | 12.15 ns | 0.2670 | 0.0038 | 4.92 KB | -| ValidateNestedArray | 321.5 ns | 75.48 ns | 4.14 ns | 0.1040 | - | 1.91 KB | -| ValidateStringWithManyRefinements | 987,030.7 ns | 67,218.17 ns | 3,684.45 ns | - | - | 15.13 KB | -| ValidateLargeObjectWithArrays | 38,199.8 ns | 9,842.49 ns | 539.50 ns | 6.8970 | 2.0752 | 127.64 KB | +| Method | Mean | Error | StdDev | Allocated | +|---------------------------------- |------------:|------------:|---------:|----------:| +| ValidateDeepNestedObject | 198.5 ns | 25.69 ns | 1.41 ns | - | +| ValidateWideObject | 2,217.2 ns | 343.53 ns | 18.83 ns | - | +| ValidateNestedArray | 177.7 ns | 36.18 ns | 1.98 ns | - | +| ValidateStringWithManyRefinements | 114.7 ns | 11.33 ns | 0.62 ns | - | +| ValidateLargeObjectWithArrays | 19,578.0 ns | 1,223.70 ns | 67.08 ns | - | ### Transform `IterationCount=5 LaunchCount=1 WarmupCount=3` -| Method | Mean | Error | StdDev | Gen0 | Allocated | -|------------------------ |----------:|---------:|---------:|-------:|----------:| -| TransformToLower | 22.24 ns | 0.544 ns | 0.141 ns | 0.0025 | 48 B | -| TransformToUpper | 23.18 ns | 0.940 ns | 0.244 ns | 0.0025 | 48 B | -| TransformTrim | 31.34 ns | 1.297 ns | 0.337 ns | 0.0025 | 48 B | -| TransformChained | 40.45 ns | 1.096 ns | 0.170 ns | 0.0051 | 96 B | -| TransformWithValidation | 144.83 ns | 5.236 ns | 1.360 ns | 0.0288 | 544 B | +| Method | Mean | Error | StdDev | Gen0 | Allocated | +|------------------------ |---------:|---------:|---------:|-------:|----------:| +| TransformToLower | 22.26 ns | 1.241 ns | 0.322 ns | 0.0025 | 48 B | +| TransformToUpper | 28.39 ns | 1.026 ns | 0.267 ns | 0.0025 | 48 B | +| TransformTrim | 29.10 ns | 0.525 ns | 0.136 ns | 0.0025 | 48 B | +| TransformChained | 40.71 ns | 0.794 ns | 0.123 ns | 0.0051 | 96 B | +| TransformWithValidation | 78.79 ns | 3.853 ns | 1.001 ns | 0.0058 | 112 B | ### Union `IterationCount=5 LaunchCount=1 WarmupCount=3` -| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | -|---------------------------------------- |----------:|----------:|---------:|-------:|-------:|----------:| -| ValidateUnion_String | 57.70 ns | 0.552 ns | 0.085 ns | 0.0033 | - | 64 B | -| ValidateUnion_Number | 72.18 ns | 2.130 ns | 0.330 ns | 0.0314 | - | 592 B | -| ValidateUnion_Boolean | 112.88 ns | 2.550 ns | 0.662 ns | 0.0421 | - | 792 B | -| ValidateDiscriminatedUnion_FirstOption | 233.45 ns | 6.170 ns | 1.602 ns | 0.0246 | - | 464 B | -| ValidateDiscriminatedUnion_SecondOption | 259.09 ns | 13.888 ns | 2.149 ns | 0.0443 | - | 840 B | -| ValidateUnion_Invalid | 201.83 ns | 4.354 ns | 1.131 ns | 0.0722 | 0.0002 | 1360 B | +| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +|---------------------------------------- |----------:|---------:|---------:|-------:|-------:|----------:| +| ValidateUnion_String | 48.70 ns | 2.309 ns | 0.600 ns | - | - | - | +| ValidateUnion_Number | 69.38 ns | 2.473 ns | 0.383 ns | 0.0314 | - | 592 B | +| ValidateUnion_Boolean | 99.71 ns | 4.742 ns | 1.231 ns | 0.0421 | - | 792 B | +| ValidateDiscriminatedUnion_FirstOption | 169.33 ns | 7.262 ns | 1.886 ns | - | - | - | +| ValidateDiscriminatedUnion_SecondOption | 171.90 ns | 3.450 ns | 0.896 ns | - | - | - | +| ValidateUnion_Invalid | 195.87 ns | 5.493 ns | 1.427 ns | 0.0722 | 0.0002 | 1360 B | ### Memory `IterationCount=10 LaunchCount=1 WarmupCount=3` -| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | -|------------------------------ |--------------:|-------------:|-------------:|---------:|--------:|--------:|-------:|----------:|------------:| -| ValidateString_Allocations | 42.31 ns | 0.774 ns | 0.512 ns | 1.00 | 0.02 | - | - | - | NA | -| ValidateArray_Allocations | 1,512.42 ns | 33.074 ns | 19.682 ns | 35.75 | 0.61 | 0.4807 | 0.0019 | 9048 B | NA | -| ValidateObject_Allocations | 145.85 ns | 1.476 ns | 0.878 ns | 3.45 | 0.05 | 0.0350 | - | 664 B | NA | -| ValidateString_ManyIterations | 42,098.80 ns | 239.443 ns | 142.488 ns | 995.08 | 12.12 | - | - | - | NA | -| ValidateArray_ManyIterations | 147,362.19 ns | 4,961.440 ns | 3,281.686 ns | 3,483.18 | 84.52 | 43.2129 | 1.2207 | 814512 B | NA | -| ValidateObject_ManyIterations | 135,243.05 ns | 1,037.450 ns | 686.209 ns | 3,196.72 | 40.59 | 22.4609 | - | 424240 B | NA | +| Method | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | +|------------------------------ |--------------:|--------------:|-------------:|---------:|--------:|----------:|------------:| +| ValidateString_Allocations | 45.82 ns | 3.871 ns | 2.304 ns | 1.00 | 0.07 | - | NA | +| ValidateArray_Allocations | 1,180.24 ns | 25.234 ns | 15.016 ns | 25.82 | 1.24 | - | NA | +| ValidateObject_Allocations | 94.28 ns | 3.079 ns | 1.832 ns | 2.06 | 0.10 | - | NA | +| ValidateString_ManyIterations | 49,215.82 ns | 11,071.853 ns | 7,323.345 ns | 1,076.52 | 160.88 | - | NA | +| ValidateArray_ManyIterations | 108,752.84 ns | 1,231.489 ns | 814.553 ns | 2,378.80 | 111.36 | - | NA | +| ValidateObject_ManyIterations | 87,507.56 ns | 1,188.630 ns | 707.334 ns | 1,914.09 | 89.82 | - | NA | > `Gen0`/`Gen1` are GC collections per 1000 operations; `Allocated` is managed memory per operation. "NA" allocations mean zero bytes were measured. +> +> All valid-input paths are **zero-allocation**. The only remaining allocations are on the failure path (error collection) and the string transforms themselves (each `ToLower`/`ToUpper`/`Trim` produces a new string, which is inherent to the transform). ## Interpreting results diff --git a/src/src/Benchmarks/TransformPerformanceTests.cs b/src/src/Benchmarks/TransformPerformanceTests.cs index ed7e8d5..b74a753 100644 --- a/src/src/Benchmarks/TransformPerformanceTests.cs +++ b/src/src/Benchmarks/TransformPerformanceTests.cs @@ -15,6 +15,7 @@ public class TransformPerformanceTests readonly ZodString _toUpperSchema; readonly ZodString _trimSchema; readonly ZodString _chainedTransformSchema; + readonly ZodString _transformWithValidationSchema; public TransformPerformanceTests() { @@ -22,6 +23,7 @@ public TransformPerformanceTests() _toUpperSchema = Z.String().ToUpper(); _trimSchema = Z.String().Trim(); _chainedTransformSchema = Z.String().Trim().ToLower(); + _transformWithValidationSchema = Z.String().Min(5).Max(100).Trim().ToLower().Email(); } [Benchmark] @@ -37,9 +39,6 @@ public TransformPerformanceTests() public ValidationResult TransformChained() => _chainedTransformSchema.Validate(" HELLO WORLD "); [Benchmark] - public ValidationResult TransformWithValidation() - { - var schema = Z.String().Min(5).Max(100).Trim().ToLower().Email(); - return schema.Validate(" USER@EXAMPLE.COM "); - } + public ValidationResult TransformWithValidation() => + _transformWithValidationSchema.Validate(" USER@EXAMPLE.COM "); } diff --git a/src/src/ZodSharp/Schemas/FieldSchemaWrapper.cs b/src/src/ZodSharp/Schemas/FieldSchemaWrapper.cs index 0b09e75..1642830 100644 --- a/src/src/ZodSharp/Schemas/FieldSchemaWrapper.cs +++ b/src/src/ZodSharp/Schemas/FieldSchemaWrapper.cs @@ -26,27 +26,7 @@ public sealed class FieldSchemaWrapper(IZodSchema inner) : IZodSchema Wrap(IZodSchema schema) => new FieldSchemaWrapper(schema); /// - public ValidationResult Validate(object value) - { - if (value is null && inner is IAcceptsNull acceptsNull) - return acceptsNull.ValidateNull(); - - if (SchemaValueCoercion.TryCoerce(value, out var typedValue)) - { - var result = inner.Validate(typedValue); - return result.IsSuccess - ? ValidationResult.Success(result.Value) - : ValidationResult.Failure(result.Errors); - } - - return ValidationResult.Failure( - new ValidationError( - "invalid_type", - $"Expected {SchemaValueCoercion.GetTypeDisplayName(typeof(T))}, but got {value?.GetType().Name ?? "null"}", - [] - ) - ); - } + public ValidationResult Validate(object value) => SchemaValueCoercion.ValidateWrapped(inner, value); /// public ValueTask> ValidateAsync( diff --git a/src/src/ZodSharp/Schemas/SchemaValueCoercion.cs b/src/src/ZodSharp/Schemas/SchemaValueCoercion.cs index e781c86..c8f66ac 100644 --- a/src/src/ZodSharp/Schemas/SchemaValueCoercion.cs +++ b/src/src/ZodSharp/Schemas/SchemaValueCoercion.cs @@ -1,4 +1,5 @@ using System.Globalization; +using ZodSharp.Core; namespace ZodSharp.Schemas; @@ -11,6 +12,57 @@ static class SchemaValueCoercion { static readonly Type DoubleType = typeof(double); + /// + /// Validates a value coming through a typed-to-untyped schema wrapper, reusing the + /// original boxed when the inner schema produced an equal + /// value so value-typed fields do not allocate a fresh box per validation. + /// + public static ValidationResult ValidateWrapped(IZodSchema inner, object? value) + { + if (value is null && inner is IAcceptsNull acceptsNull) + return acceptsNull.ValidateNull(); + + if (value is T typedValue) + { + var result = inner.Validate(typedValue); + if (!result.IsSuccess) + return ValidationResult.Failure(result.Errors); + + // Reuse the original boxed value when the schema did not transform it. + return SameValue(typedValue, result.Value) + ? ValidationResult.Success(value) + : ValidationResult.Success(result.Value); + } + + if (TryCoerce(value, out var coerced)) + { + var result = inner.Validate(coerced); + return result.IsSuccess + ? ValidationResult.Success(result.Value) + : ValidationResult.Failure(result.Errors); + } + + return ValidationResult.Failure( + new ValidationError( + "invalid_type", + $"Expected {GetTypeDisplayName(typeof(T))}, but got {value?.GetType().Name ?? "null"}", + [] + ) + ); + } + + /// + /// Returns when two values are interchangeable: reference + /// types compare by reference, value types by value. + /// + static bool SameValue(T left, T right) + { + if (typeof(T).IsValueType) + return EqualityComparer.Default.Equals(left, right); + + return ReferenceEquals(left, right); + } + /// /// Attempts to coerce to . /// Returns and sets on success; diff --git a/src/src/ZodSharp/Schemas/ZodArray.cs b/src/src/ZodSharp/Schemas/ZodArray.cs index 01d2d5a..8c9d28c 100644 --- a/src/src/ZodSharp/Schemas/ZodArray.cs +++ b/src/src/ZodSharp/Schemas/ZodArray.cs @@ -33,14 +33,15 @@ protected override ValidationResult ParseInternal(T[] value) ); } - List errors = [with(value.Length)]; - List validatedItems = [with(value.Length)]; + List? errors = null; + List? rebuilt = null; for (var i = 0; i < value.Length; i++) { var itemResult = elementSchema.Validate(value[i]); if (!itemResult.IsSuccess) { + errors ??= []; foreach (var error in itemResult.Errors) { var path = new string[error.Path.Length + 1]; @@ -50,32 +51,61 @@ protected override ValidationResult ParseInternal(T[] value) } } else - validatedItems.Add(itemResult.Value); + { + var itemValue = itemResult.Value; + if (rebuilt is null && !SameValue(itemValue, value[i])) + { + rebuilt = [with(value.Length)]; + for (var j = 0; j < i; j++) + rebuilt.Add(value[j]); + } + + rebuilt?.Add(itemValue); + } } - if (errors.Count > 0) + if (errors is { Count: > 0 }) return ValidationResult.Failure(errors); - // When no errors... - return _minLength.HasValue && validatedItems.Count < _minLength.Value - ? ValidationResult.Failure( - new ValidationError( - "too_small", - _errorMessage - ?? $"Array must have at least {_minLength.Value} elements, but got {validatedItems.Count}", - EmptyPath - ) + var count = value.Length; + if (_minLength.HasValue && count < _minLength.Value) + { + return ValidationResult.Failure( + new ValidationError( + "too_small", + _errorMessage ?? $"Array must have at least {_minLength.Value} elements, but got {count}", + EmptyPath ) - : _maxLength.HasValue && validatedItems.Count > _maxLength.Value - ? ValidationResult.Failure( - new ValidationError( - "too_big", - _errorMessage - ?? $"Array must have at most {_maxLength.Value} elements, but got {validatedItems.Count}", - EmptyPath - ) + ); + } + + if (_maxLength.HasValue && count > _maxLength.Value) + { + return ValidationResult.Failure( + new ValidationError( + "too_big", + _errorMessage ?? $"Array must have at most {_maxLength.Value} elements, but got {count}", + EmptyPath ) - : ValidationResult.Success([.. validatedItems]); + ); + } + + // When every element passed through unchanged, the input array is already + // the validated result, so reuse it instead of copying. + return rebuilt is not null ? ValidationResult.Success([.. rebuilt]) : ValidationResult.Success(value); + } + + /// + /// Returns when two element values are interchangeable, + /// i.e. no transform produced a different value. Reference types compare by + /// reference; value types by value (so a freshly boxed value still matches). + /// + static bool SameValue(T left, T right) + { + if (typeof(T).IsValueType) + return EqualityComparer.Default.Equals(left, right); + + return ReferenceEquals(left, right); } /// diff --git a/src/src/ZodSharp/Schemas/ZodObject.cs b/src/src/ZodSharp/Schemas/ZodObject.cs index ec7a6e4..734830f 100644 --- a/src/src/ZodSharp/Schemas/ZodObject.cs +++ b/src/src/ZodSharp/Schemas/ZodObject.cs @@ -65,97 +65,151 @@ public class ZodObject( ); } - var shapeCount = shape.Count; - List errors = [with(shapeCount)]; - Dictionary validatedObject = [with(shapeCount)]; + List? errors = null; + Dictionary? rebuilt = null; foreach (var (key, schema) in shape) + ValidateField(value, key, schema, ref errors, ref rebuilt); + + foreach (var (key, propertyValue) in value) + ValidateUnknownKey(value, key, propertyValue, ref errors, ref rebuilt); + + if (errors is { Count: > 0 }) + return ValidationResult>.Failure(errors); + + // When every field passed through unchanged and nothing needed stripping or + // injecting, the input dictionary is already the validated result. + return rebuilt is not null + ? ValidationResult>.Success(rebuilt) + : ValidationResult>.Success(value); + } + + /// + /// Validates a single field from the shape against the input value, recording any + /// error or, when the validated value differs (transform/default), triggering a + /// rebuild of the output object. + /// + void ValidateField( + Dictionary value, + string key, + IZodSchema schema, + ref List? errors, + ref Dictionary? rebuilt + ) + { + if (!value.TryGetValue(key, out var propertyValue)) { - if (!value.TryGetValue(key, out var propertyValue)) + var allowsMissing = + !IsRequiredKey(key) && (IsOptionalKey(key) || schema is IOptionalSchema { IsOptional: true }); + + if (!allowsMissing) { - var allowsMissing = - !IsRequiredKey(key) && (IsOptionalKey(key) || schema is IOptionalSchema { IsOptional: true }); + errors ??= []; + errors.Add(new ValidationError("missing_field", $"Required field '{key}' is missing", [key])); + return; + } - if (!allowsMissing) - { - errors.Add(new ValidationError("missing_field", $"Required field '{key}' is missing", [key])); - continue; - } + if (schema is IOptionalSchema { ProvidesValueOnMissing: true }) + { + var missingResult = schema.Validate(null!); + if (missingResult.IsSuccess && missingResult.Value is not null) + GetRebuilt(value, ref rebuilt)[key] = missingResult.Value; + } - if (schema is IOptionalSchema { ProvidesValueOnMissing: true }) - { - var missingResult = schema.Validate(null!); - if (missingResult.IsSuccess && missingResult.Value is not null) - validatedObject[key] = missingResult.Value; - } + return; + } + + var result = schema.Validate(propertyValue!); - continue; + if (!result.IsSuccess) + { + errors ??= []; + foreach (var error in result.Errors) + { + var path = new string[error.Path.Length + 1]; + path[0] = key; + + error.Path.CopyTo(0, path, 1, error.Path.Length); + errors.Add(new(error.Code, error.Message, path, error.Parameters)); } - var result = schema.Validate(propertyValue!); + return; + } + + if (!Equals(result.Value, propertyValue)) + GetRebuilt(value, ref rebuilt)[key] = result.Value; + } + + /// + /// Handles a key that is not part of the shape according to the unknown-key policy. + /// + void ValidateUnknownKey( + Dictionary value, + string key, + object? propertyValue, + ref List? errors, + ref Dictionary? rebuilt + ) + { + if (shape.ContainsKey(key)) + return; - if (!result.IsSuccess) + if (catchallSchema is not null) + { + var catchallResult = catchallSchema.Validate(propertyValue!); + if (!catchallResult.IsSuccess) { - foreach (var error in result.Errors) + errors ??= []; + foreach (var error in catchallResult.Errors) { var path = new string[error.Path.Length + 1]; path[0] = key; - error.Path.CopyTo(0, path, 1, error.Path.Length); errors.Add(new(error.Code, error.Message, path, error.Parameters)); } } else { - validatedObject[key] = result.Value; + GetRebuilt(value, ref rebuilt)[key] = catchallResult.Value; } + + return; } - // Handle unknown keys (keys not in the shape). - foreach (var (key, propertyValue) in value) +#pragma warning disable IDE0010 // Add missing cases + switch (unknownKeyPolicy) { - if (shape.ContainsKey(key)) - continue; + case UnknownKeyPolicy.Passthrough: + break; + case UnknownKeyPolicy.Strict: + errors ??= []; + errors.Add(new ValidationError("unrecognized_key", $"Unrecognized key '{key}'", [key])); + break; + default: + // Strip: drop the unknown key by forcing the rebuilt copy. + GetRebuilt(value, ref rebuilt); + break; + } +#pragma warning restore IDE0010 // Add missing cases + } - if (catchallSchema is not null) - { - var catchallResult = catchallSchema.Validate(propertyValue!); - if (!catchallResult.IsSuccess) - { - foreach (var error in catchallResult.Errors) - { - var path = new string[error.Path.Length + 1]; - path[0] = key; - error.Path.CopyTo(0, path, 1, error.Path.Length); - errors.Add(new(error.Code, error.Message, path, error.Parameters)); - } - } - else - { - validatedObject[key] = catchallResult.Value; - } - } - else + /// + /// Creates (once) a copy of the input containing only the shape keys. Unknown keys + /// are dropped as a side effect, which is exactly what strip semantics need. + /// + Dictionary GetRebuilt(Dictionary value, ref Dictionary? rebuilt) + { + if (rebuilt is null) + { + rebuilt = [with(shape.Count)]; + foreach (var (key, propertyValue) in value) { -#pragma warning disable IDE0010 // Add missing cases - switch (unknownKeyPolicy) - { - case UnknownKeyPolicy.Passthrough: - validatedObject[key] = propertyValue; - break; - case UnknownKeyPolicy.Strict: - errors.Add(new ValidationError("unrecognized_key", $"Unrecognized key '{key}'", [key])); - break; - default: - break; - } -#pragma warning restore IDE0010 // Add missing cases + if (shape.ContainsKey(key)) + rebuilt[key] = propertyValue; } } - return errors.Count > 0 - ? ValidationResult>.Failure(errors) - : ValidationResult>.Success(validatedObject); + return rebuilt; } /// diff --git a/src/src/ZodSharp/Schemas/ZodObjectBuilder.cs b/src/src/ZodSharp/Schemas/ZodObjectBuilder.cs index a135dda..e4cd820 100644 --- a/src/src/ZodSharp/Schemas/ZodObjectBuilder.cs +++ b/src/src/ZodSharp/Schemas/ZodObjectBuilder.cs @@ -41,27 +41,7 @@ sealed class SchemaWrapper(IZodSchema inner) : IZodSchema inner is IOptionalSchema o && o.ProvidesValueOnMissing; - public ValidationResult Validate(object value) - { - if (value is null && inner is IAcceptsNull acceptsNull) - return acceptsNull.ValidateNull(); - - if (SchemaValueCoercion.TryCoerce(value, out var typedValue)) - { - var result = inner.Validate(typedValue); - return result.IsSuccess - ? ValidationResult.Success(result.Value) - : ValidationResult.Failure(result.Errors); - } - - return ValidationResult.Failure( - new ValidationError( - "invalid_type", - $"Expected {SchemaValueCoercion.GetTypeDisplayName(typeof(T))}, but got {value?.GetType().Name ?? "null"}", - [] - ) - ); - } + public ValidationResult Validate(object value) => SchemaValueCoercion.ValidateWrapped(inner, value); public ValueTask> ValidateAsync( object value, diff --git a/src/src/ZodSharp/Schemas/ZodUnion.cs b/src/src/ZodSharp/Schemas/ZodUnion.cs index fda7d42..259e9ac 100644 --- a/src/src/ZodSharp/Schemas/ZodUnion.cs +++ b/src/src/ZodSharp/Schemas/ZodUnion.cs @@ -18,7 +18,7 @@ public class ZodUnion(IReadOnlyList> options) : ZodTy /// A validation result protected override ValidationResult ParseInternal(object value) { - List allErrors = []; + List? allErrors = null; foreach (var option in options) { @@ -28,6 +28,7 @@ protected override ValidationResult ParseInternal(object value) return result; } + allErrors ??= []; allErrors.AddRange(result.Errors); } @@ -36,7 +37,7 @@ protected override ValidationResult ParseInternal(object value) "invalid_union", "Value does not match any of the union options", [], - new Dictionary { { "errors", allErrors } } + new Dictionary { { "errors", allErrors ?? [] } } ) ); }