diff --git a/docs/wiki/Arrays-and-Other-Schemas.md b/docs/wiki/Arrays-and-Other-Schemas.md new file mode 100644 index 0000000..cf1aa79 --- /dev/null +++ b/docs/wiki/Arrays-and-Other-Schemas.md @@ -0,0 +1,103 @@ +# Arrays and Other Schemas + +## Arrays + +`ZodArray` (namespace `ZodSharp.Schemas`) validates `T[]` values. Each element is validated against the element schema; failures carry index paths such as `["0"]`. A rebuilt array is produced only when a transform changed an element. + +```csharp +using ZodSharp; + +var numbers = Z.Array(Z.Number()).Min(1).Max(10); +var result = numbers.Validate(new[] { 1.0, 2.0, 3.0 }); +``` + +| Method | Behaviour | +|---|---| +| `Min(int minLength, string? message)` | `too_small` when count < min | +| `Max(int maxLength, string? message)` | `too_big` when count > max | +| `Length(int length, string? message)` | exact length (both bounds) | +| `NonEmpty(string? message)` | `minLength = 1` | + +## Boolean + +`ZodBoolean` validates `bool`; there are no fluent methods. + +```csharp +var schema = Z.Boolean(); +``` + +## Null + +`ZodNull` succeeds only for `null` input, and implements `IAcceptsNull` so it can serve as an object field accepting `null`. + +```csharp +var schema = Z.Null(); +``` + +## Enum (string values) + +`ZodEnum` validates against a set of allowed strings. Failure produces `invalid_enum_value`. + +```csharp +var schema = Z.Enum("admin", "user", "guest"); +``` + +## Native enum + +`ZodNativeEnum` validates a native `System.Enum` using `Enum.IsDefined`. Failure produces `invalid_enum_value`. + +```csharp +var schema = Z.Enum(); // Color : struct, Enum +``` + +## Literal + +`ZodLiteral` (where `T : IEquatable`) accepts exactly one value. Failure produces `invalid_literal`. + +```csharp +var schema = Z.Literal("active"); +var schema2 = Z.Literal(42); +``` + +## Record + +`ZodRecord` validates `Dictionary`, validating every value against the value schema with key-prefixed error paths. It always produces a fresh validated dictionary. + +```csharp +var schema = Z.Record(Z.Number()); +``` + +## Tuple + +`ZodTuple` validates fixed-length tuples. Two- and three-element overloads exist; input is `object?[]`. + +```csharp +var schema = Z.Tuple(Z.String(), Z.Number()); + +var result = schema.Validate(new object?[] { "John", 30.0 }); +// (string, double) success value +``` + +Failures: `invalid_type` on `null`, `invalid_tuple_length` on wrong length, and per-index `invalid_type` with paths like `["[0]"]`. + +## Lazy + +`ZodLazy` defers schema construction to first use, enabling recursive and circular schemas. The inner `Schema` is resolved lazily and thread-safely. + +```csharp +var categorySchema = Z.Lazy>(() => + Z.Object() + .Field("name", Z.String()) + .Field("subcategories", Z.Array(categorySchema)) + .Build()); +``` + +## Optional / Nullable + +`Z.Optional(schema)` (`T : class`) accepts `null` or a value matching the inner schema. `Z.Nullable(schema)` (`T : struct`) is the value-type counterpart. + +```csharp +var optional = Z.Optional(Z.String()); +optional.Validate(null); // Success +optional.Validate("value"); // Success +``` \ No newline at end of file diff --git a/docs/wiki/AspNetCore-Integration.md b/docs/wiki/AspNetCore-Integration.md new file mode 100644 index 0000000..4c2dc32 --- /dev/null +++ b/docs/wiki/AspNetCore-Integration.md @@ -0,0 +1,66 @@ +# ASP.NET Core Integration + +The `Purview.ZodSharp.AspNetCore` package converts failed validation results into standard `ProblemDetails` / `HttpValidationProblemDetails` payloads while preserving the structured validation issues. It also registers ZodSharp schema resolution into your application's dependency injection container. + +## Install + +```bash +dotnet add package Purview.ZodSharp.AspNetCore +``` + +## ProblemDetails + +```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"], + }); +} +``` + +| Member | Behaviour | +|---|---| +| `ToHttpValidationProblemDetails(ValidationResult result, int statusCode = 400)` | `HttpValidationProblemDetails`; error paths flattened to dotted keys (`user.email`, array indexes as `[0]`) | +| `ToValidationProblemDetails(ValidationResult result, int statusCode = 400)` | `ValidationProblemDetails` | + +Both throw `InvalidOperationException` when the result `IsSuccess`. The structured metadata is preserved in the `issues` extension as a `ValidationIssue[]`: + +```csharp +public sealed class ValidationIssue +{ + public required string Code { get; init; } + public string? Origin { get; init; } + public int? Minimum { get; init; } + public int? Maximum { get; init; } + public bool? Inclusive { get; init; } + public required string[] Path { get; init; } + public required string Message { get; init; } +} +``` + +## Dependency injection + +```csharp +builder.Services.AddZodSharp(options => +{ + options.ScanAssemblies.Add(typeof(UserDto).Assembly); +}); +``` + +`AddZodSharp(Action? configure)` registers `IZodSchemaFactory` as a singleton, applies `options.ConfigureFactory`, and calls `factory.RegisterFromAssembly(assembly)` for each entry in `options.ScanAssemblies`. This auto-discovers source-generated `[assembly: ZodSchemaGenerated(typeof(...))]` registrations. + +`ZodSchemaFactoryOptions`: + +- `List ScanAssemblies` — assemblies to scan for generated schemas. +- `Action? ConfigureFactory` — additional factory configuration. + +See [Dependency Injection](Dependency-Injection.md) for the underlying factory and options-validation wiring. \ No newline at end of file diff --git a/docs/wiki/Compiled-Validators-and-Caching.md b/docs/wiki/Compiled-Validators-and-Caching.md new file mode 100644 index 0000000..d2ec690 --- /dev/null +++ b/docs/wiki/Compiled-Validators-and-Caching.md @@ -0,0 +1,48 @@ +# Compiled Validators and Caching + +## CompiledValidator + +`CompiledValidator` (namespace `ZodSharp.Expressions`) compiles a schema into an expression tree and a delegate. + +```csharp +using ZodSharp; +using ZodSharp.Expressions; + +var compiled = CompiledValidator.Compile(schema); +var result = compiled(value); // ValidationResult + +var parser = CompiledValidator.CompileParser(schema); +var value = parser(input); // T, throws ZodException on failure +``` + +| Member | Signature | Returns | +|---|---|---| +| `Compile` | `Func> Compile(IZodSchema schema)` | compiled validation delegate | +| `CompileParser` | `Func CompileParser(IZodSchema schema)` | returns the value or throws `ZodException` | + +The expression tree calls `IZodSchema.Validate` on the schema bound as a constant, removing interface dispatch overhead. + +## SchemaCache + +`SchemaCache` (namespace `ZodSharp.Core`) is a `ConcurrentDictionary`-backed cache for expensive schema construction. + +```csharp +using ZodSharp.Core; + +var schema = SchemaCache.GetOrCreate("user", () => + Z.Object().Field("name", Z.String()).Build()); +``` + +| Member | Behaviour | +|---|---| +| `GetOrCreate(string key, Func factory)` | returns the cached instance or creates and stores it (`T : class`) | +| `TryGet(string key, out T value)` | typed lookup | +| `Remove(string key)` | removes an entry | +| `Count` | number of cached entries | +| `Clear()` | empties the cache | + +Schemas are immutable and shareable, so caching identical definitions avoids repeated construction cost across request boundaries. + +## The source generator alternative + +For the highest performance, prefer the compile-time source generator: `[ZodSchema]` emits a static validator with no runtime compilation or dispatch overhead. See [Source Generator](Source-Generator.md). \ No newline at end of file diff --git a/docs/wiki/Composition-and-Transforms.md b/docs/wiki/Composition-and-Transforms.md new file mode 100644 index 0000000..e7d3093 --- /dev/null +++ b/docs/wiki/Composition-and-Transforms.md @@ -0,0 +1,109 @@ +# Composition and Transforms + +Every schema derives from `ZodType`, so the composition methods below are available on every schema (guarded so they only apply when input equals output). Each method returns a **new** wrapping schema; the original is unchanged. + +## Transform + +```csharp +var schema = Z.String().Transform(s => s.ToUpperInvariant()); +var result = schema.Validate("hello"); // "HELLO" +``` + +`ZodTransform` validates the input schema first, then runs the transform. If the transform throws, the exception is caught into a `transform_error` `ValidationError` (`"Transform failed: {message}"`). + +Transforms chain: + +```csharp +var schema = Z.String().Transform(s => s.Trim()).Transform(s => s.ToUpperInvariant()); +``` + +`ZodString` ships convenience transforms: `.ToLower()`, `.ToUpper()`, and `.Trim()`. + +## Refine + +`ZodRefinement` runs the base schema first, then a predicate. A failing predicate produces code `refinement_failed` (custom `message` or `"Custom validation failed"`). + +```csharp +var even = Z.Number().Refine(n => n % 2 == 0, "Must be even"); +``` + +## SuperRefine + +`ZodSuperRefinement` takes an `Action>` and can emit multiple, path-located issues. + +```csharp +var password = Z.String().SuperRefine(ctx => +{ + if (!ctx.Value.Any(char.IsUpper)) + ctx.AddIssue("Must contain an uppercase letter", new[] { "uppercase" }); + if (ctx.Value.Length < 8) + ctx.AddIssue("too_short", "Must be at least 8 characters", new[] { "length" }); +}); +``` + +`RefineCtx` exposes: + +- `T Value` — the value being refined. +- `ImmutableArray Path` — the base path. +- `AddIssue(string code, string message, string[]? path)` — appends to the base path; throws `ArgumentException` for null/whitespace code or message. +- `AddIssue(string message, string[]? path)` — shorthand with code `refinement_failed`. +- `Issues` / `HasIssues`. + +## Pipe + +`ZodPipe` runs the source schema, then validates its output against a target schema. + +```csharp +var schema = Z.String().Pipe(Z.String().Min(10)); +``` + +## Catch + +`ZodCatch` swallows inner failures and returns a fallback as a **successful** result. The fallback can be a constant or a factory that receives the input and the errors. + +```csharp +var withFallback = Z.String().Catch("n/a"); +var computed = Z.Number().Catch((value, errors) => 0); +``` + +## Default + +`ZodDefault` substitutes a value when input is `null` and reports `IsOptional = true` and `ProvidesValueOnMissing = true`. The default is **not** re-validated. + +```csharp +var schema = Z.String().Default("unknown"); +var result = schema.Validate(null); // "unknown" +``` + +## Prefault + +`ZodPrefault` substitutes a value when the input equals `default(T)`, then **still validates** the substituted value through the inner schema. + +```csharp +var schema = Z.Number().Prefault(1); +``` + +## And / Or + +- `.And(other)` → `ZodIntersection` — both must succeed (see [Unions and Discriminated Unions](Unions-and-Discriminated-Unions.md)). +- `.Or(other)` → `ZodTypedUnion` — either may succeed. + +## Example: layered validation + +```csharp +var schema = Z.String() + .Min(3) + .Transform(s => s.Trim()) + .Refine(s => s.StartsWith("PUR-", StringComparison.Ordinal), "Must start with PUR-") + .Default("PUR-UNKNOWN"); +``` + +## Behavioral differences at a glance + +| Wrapper | Trigger | Re-validates substituted value | +|---|---|---| +| `Default(value)` | `null` input | No | +| `Prefault(value)` | input equals `default(T)` | Yes | +| `Catch(value\|factory)` | inner schema fails | No (returns fallback as success) | +| `Refine(predicate)` | predicate returns false | — | +| `SuperRefine(action)` | issues added to context | — | \ No newline at end of file diff --git a/docs/wiki/Contributing.md b/docs/wiki/Contributing.md new file mode 100644 index 0000000..82d7284 --- /dev/null +++ b/docs/wiki/Contributing.md @@ -0,0 +1,68 @@ +# Contributing + +Contributions are welcome. Please open an issue or pull request against [purview-dev/zodsharp](https://github.com/purview-dev/zodsharp). + +## Repository layout + +``` +src/ + 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 + 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 +src/ts/ TypeScript (Zod) schema + fixture generation +tests/ts/ Vitest cross-platform tests +docs/wiki/ This documentation suite +``` + +## Commands + +```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 Release +``` + +TypeScript 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 +``` + +## Testing bar + +- Framework: **TUnit**. +- `[Test]` methods take a `CancellationToken cancellationToken` parameter where relevant. +- Use `// Arrange`, `// Act`, `// Assert` comments. +- Use meaningful, descriptive names (`Action_GivenCondition_ExpectedResult`). +- Treat work as incomplete until the relevant tests pass. + +For source generator tests, use the `Purview.SourceGeneratorFramework.Testing.TUnit` base classes (`TUnitSourceGeneratorTestBase`, `TUnitDiagnosticAnalyzerTestBase`) and assert with `CodeQuery`; test incrementally, not just generated text. + +## Documentation + +This wiki lives in `docs/wiki/`. The site build (purview-dev Astro/Starlight) pulls these files via `github-path` sync and rewrites relative `.md` links into site routes. + +Conventions: + +- `_Sidebar.md` declares page order (parsed by the sync, never rendered). +- Every page starts with a single `# ` heading (the page title) followed by a one-paragraph description. +- Link to other pages with relative `.md` links: `[Getting Started](Getting-Started.md)`. +- GitHub alert blockquotes (`> [!NOTE]`, `> [!TIP]`, `> [!WARNING]`, `> [!CAUTION]`, `> [!IMPORTANT]`) are converted to Starlight asides. +- Relative repository-path links (e.g. `src/src/ZodSharp/Z.cs`) are rewritten to GitHub blob URLs automatically. +- Keep the `CrossPlatformUserSchema` (C#) and `UserSchema` (TypeScript) in sync when either changes. + +## Formatting + +Formatting is enforced with CSharpier. `.editorconfig` at the repo root defines style (tabs for code, 2-space for XML/JSON/YAML/markdown). \ No newline at end of file diff --git a/docs/wiki/Core-Concepts.md b/docs/wiki/Core-Concepts.md new file mode 100644 index 0000000..910314c --- /dev/null +++ b/docs/wiki/Core-Concepts.md @@ -0,0 +1,105 @@ +# Core Concepts + +## The validation pipeline + +Every schema derives from `ZodType` (namespace `ZodSharp.Core`). Validation is a two-phase pipeline: + +1. **ParseInternal** — each schema overrides this hook to perform its type check and traversal (rejecting `null` where not allowed, coercing types, walking objects/arrays/tuples/unions, producing structured failures). +2. **Rules** — on success, the accumulated `IValidationRule` structs are evaluated. A failing rule emits a `ValidationError` with code `"validation_failed"`. + +```csharp +ValidationResult result = schema.Validate(value); +``` + +## Validate, SafeParse, Parse, ValidateAsync + +| Member | Behaviour | +|---|---| +| `Validate(TInput value)` | Returns a `ValidationResult`. Never throws. | +| `SafeParse(TInput value)` | Alias of `Validate`. | +| `Parse(TInput value)` | Returns the validated `TOutput`, or throws `ZodException` on failure (via `GetValueOrThrow()`). | +| `ValidateAsync(TInput value, CancellationToken)` | `ValueTask` wrapper around `Validate`. The pipeline is synchronous; this exists for interface symmetry and the source generator's custom async validation. | + +## ValidationResult + +`ValidationResult` is a `readonly record struct` in `ZodSharp.Core`: + +- `bool IsSuccess` — annotated `[MemberNotNullWhen(true, nameof(Value))]`. +- `T? Value` — the validated value; valid only when `IsSuccess`. +- `ImmutableArray Errors` — populated on failure. + +Static factories: `Success(value)`, `Failure(ValidationError)`, `Failure(ImmutableArray)`, `Failure(IEnumerable)`, and `Merge(lhs, rhs)` (succeeds only if both succeed; concatenates errors). + +## ValidationError + +`ValidationError` is a `readonly record struct` carrying machine-readable metadata: + +- `Code` — e.g. `"invalid_type"`, `"too_small"`, `"too_big"`, `"invalid_union"`, `"missing_field"`, `"unrecognized_key"`, `"validation_failed"`, `"refinement_failed"`, `"transform_error"`. +- `Message` — human-readable message. +- `Path` — `ImmutableArray`, e.g. `["user", "email"]`; array indexes appear as string segments such as `"0"`. +- `Origin` — category for structured size issues (`"string"`, `"array"`, `"collection"`). +- `Minimum` / `Maximum` — inclusive bounds for size issues. +- `Inclusive` — whether the bound is inclusive. +- `Parameters` — `IReadOnlyDictionary`, e.g. union failures carry every option error under `"errors"`. + +Create custom issues with `ValidationError.Create(code, message, path, parameters, origin, minimum, maximum, inclusive)`. + +## ZodException + +`ZodException` (namespace `ZodSharp.Core`) is thrown by `Parse` and `GetValueOrThrow()`. It exposes `ImmutableArray Errors`. Its `ToString()` renders one line per error: `"{joinedPath}: {Message} ({Code})"`. + +```csharp +try +{ + var value = schema.Parse("AB"); +} +catch (ZodException ex) +{ + foreach (var error in ex.Errors) + Console.WriteLine($"{string.Join(".", error.Path)}: {error.Message}"); +} +``` + +## Schemas and rules + +- Schemas are classes deriving from `ZodType`; the fluent methods return `this` (or a wrapping schema) so chains read naturally. +- Rules are `readonly record struct` implementations of `IValidationRule` (`bool IsValid(in T value)`, `string GetErrorMessage(in T value)`) — zero allocation. +- `ValidateSpan(ReadOnlySpan value)` is available on `ZodString` for span-based validation. +- A schema's `Description` is set with `.Describe("...")`. + +## Composition model + +`ZodType` composes via wrappers rather than mutation. The base type guards that input and output types match, then returns a new schema: + +- `Transform(Func)` → `ZodTransform`. +- `Refine(Func, string? message)` → `ZodRefinement`. +- `SuperRefine(Action>)` → `ZodSuperRefinement`. +- `Pipe(IZodSchema)` → `ZodPipe`. +- `Catch(TOutput | Func, TOutput>)` → `ZodCatch`. +- `Prefault(TOutput)` → `ZodPrefault`. +- `Default(TOutput)` → `ZodDefault`. +- `And(IZodSchema)` → `ZodIntersection`. +- `Or(IZodSchema)` → `ZodTypedUnion`. + +See [Composition and Transforms](Composition-and-Transforms.md) for details and semantics. + +## Optionality and null handling + +Schemas report optionality through the internal `IOptionalSchema` interface: + +- `IsOptional` — `true` for `ZodOptional`, `ZodNullable`, `ZodDefault`, `ZodPrefault`. +- `ProvidesValueOnMissing` — `true` for `ZodDefault` and `ZodPrefault`; object fields route missing values through `Validate(null!)` to inject the produced value. +- `IAcceptsNull.ValidateNull()` is implemented by nullable/optional/default/prefault schemas and `ZodNull`; object-field and union wrappers route `null` input to it instead of failing coercion. + +## Type coercion + +Object fields and union options wrap typed schemas so boxed values from a `Dictionary` can be validated: + +- `null` routes to `IAcceptsNull.ValidateNull()` where supported. +- Exact-typed values reuse the original boxed value. +- Numeric coercion uses `IConvertible` (invariant culture), so a boxed `long` can satisfy a `Z.Number()` field. +- Non-nullable value types reject `null`; anything else falls through to failure. + +## Dependency injection + +`IZodSchemaFactory` is the registry that resolves validators by type. See [Dependency Injection](Dependency-Injection.md). \ No newline at end of file diff --git a/docs/wiki/Cross-Platform-Interop.md b/docs/wiki/Cross-Platform-Interop.md new file mode 100644 index 0000000..24a9db9 --- /dev/null +++ b/docs/wiki/Cross-Platform-Interop.md @@ -0,0 +1,55 @@ +# Cross-Platform Interop + +ZodSharp ships a cross-platform fixture pipeline that proves the C# implementation agrees with TypeScript/Zod. The TypeScript side runs on [Bun](https://bun.sh); the C# side runs under the TUnit test suite. + +## The schema + +Both sides define the same user schema — TypeScript `UserSchema` in `src/ts/schema.ts` and the C# `CrossPlatformUserSchema` in `src/tests/SystemTextJson.UnitTests/CrossPlatformFixtures.cs` (mirrored by `NewtonsoftJson.UnitTests`): + +| Field | Zod (TS) | ZodSharp (C#) | +|---|---|---| +| `name` | `z.string().min(1)` | min-length 1 | +| `age` | `z.number().int().min(0).max(120)` | `0..120` integer | +| `email` | `z.string().email().optional()` | optional email | +| `tags` | `z.array(z.string()).default([])` | string array, default `[]` | + +## Fixture generation + +```bash +bun run generate-fixtures # runs src/ts/generate-fixtures.ts +``` + +For each of the eight fixture cases (three valid, five invalid) the script: + +1. Writes a JSON file to `src/ts/fixtures/{key}.json`. +2. Runs `UserSchema.safeParse(value)` and records the outcome in `src/ts/fixtures/manifest.json` — the authoritative `{ "valid": true|false }` result for every fixture. + +## The validation loop + +1. **`bun run generate-fixtures`** writes `src/ts/fixtures/*.json` and `manifest.json` (Zod is the authority on validity). +2. **`just test`** (C# TUnit) reads the fixtures and manifest, asserts outcomes match, and writes validated C# output to `src/tests/cross-platform/output/{systemtext,newtonsoft}-valid.json`. +3. **`bun run test`** (vitest) re-checks fixture validity under Zod and parses the C# output JSON — closing the TypeScript ↔ C# loop. + +**C# cross-platform tests** (`SystemTextCrossPlatformTests`, `NewtonsoftCrossPlatformTests`) load every fixture and assert the C# outcome matches `manifest.json`, deserialize-and-validate valid fixtures, round-trip TS fixture → C# → JSON → C#, and serialize a validated `CrossPlatformUser` into `src/tests/cross-platform/output/{systemtext,newtonsoft}-valid.json`. + +**Vitest cross-platform tests** (`tests/ts/cross-platform.test.ts`) re-check fixture validity under Zod, verify canonical serialization, and parse every JSON file in `cross-platform/output` with Zod to prove C# output is acceptable to TypeScript/Zod. + +If the C# output directory is empty, the vitest suite emits a note instructing you to run the C# cross-platform tests first. + +## JSON Schema bridge + +The same interop goal is available without fixtures via JSON Schema: + +- Export: `Z.ToJsonSchema` (core package) → JSON Schema, or `z.toJSONSchema` on the TypeScript side (Zod v4+). +- Import: `Z.FromJsonSchema` (in the System.Text.Json or Newtonsoft.Json package). + +See [JSON Schema Export](JsonSchema-Export.md) and [JSON Schema Import](JsonSchema-Import.md). + +## Directory layout + +``` +src/ts/ TypeScript (Zod) schemas + fixture generation +src/ts/fixtures/ generated JSON fixtures + manifest.json +tests/ts/ vitest cross-platform tests +src/tests/cross-platform/ shared output directory for C#-generated JSON +``` \ No newline at end of file diff --git a/docs/wiki/Dependency-Injection.md b/docs/wiki/Dependency-Injection.md new file mode 100644 index 0000000..148fb3f --- /dev/null +++ b/docs/wiki/Dependency-Injection.md @@ -0,0 +1,75 @@ +# Dependency Injection + +ZodSharp can resolve validators through an `IZodSchemaFactory` registry, and can validate options objects through `IValidateOptions`. + +## IZodSchemaFactory + +`IZodSchemaFactory` (namespace `ZodSharp.Core`) resolves validators by type: + +| Member | Behaviour | +|---|---| +| `Resolve()` | returns `IZodSchemaValidator?` or `null` when unregistered | +| `ResolveRequired()` | returns the validator or throws `InvalidOperationException` | +| `Validate(T value)` | validates through the registered validator for `T` | +| `Register(IZodSchemaValidator)` | registers a validator | +| `Register(Type, IZodSchemaValidator)` | non-generic registration | +| `TryRegister(IZodSchemaValidator)` | returns `false` if already registered | +| `IsRegistered()` | checks for a registration | + +The default implementation is `ZodSchemaFactory` (concurrent dictionary keyed by `Type`). + +`IZodSchemaValidator` extends `IZodSchema`, so a resolved validator validates like any other schema. Hand-built schemas can be registered by wrapping them: + +```csharp +using ZodSharp.Core; + +factory.Register(new ZodSchemaValidator>(myObjectSchema)); +``` + +## Registering source-generated validators + +The source generator emits `[assembly: ZodSchemaGenerated(typeof({Type}))]` attributes and a `{Type}SchemaValidator` adapter. Scan an assembly to register every generated validator: + +```csharp +factory.RegisterFromAssembly(typeof(User).Assembly); +// or +factory.RegisterFromAssembly(); +``` + +`ZodSchemaFactoryExtensions.RegisterFromAssembly` looks up `{TypeName}SchemaValidator` in the target type's namespace/assembly and registers it. It throws `InvalidOperationException` when the validator type or `IZodSchemaValidator` implementation is missing. + +## Registering the factory in DI + +The core package provides a `Microsoft.Extensions.DependencyInjection` extension: + +```csharp +builder.Services.AddZodSharpFactory(factory => factory.RegisterFromAssembly(typeof(User).Assembly)); +``` + +`AddZodSharpFactory(Action? configure = null)` registers a singleton `IZodSchemaFactory` and invokes the configuration callback. + +The `Purview.ZodSharp.AspNetCore` package offers the richer `AddZodSharp` with `ScanAssemblies` — see [ASP.NET Core Integration](AspNetCore-Integration.md). + +## Validating options objects + +Wire generated validators into the options framework so invalid configuration fails fast at startup: + +```csharp +builder.Services.AddZodSchemaOptionsValidator(); +``` + +`AddZodSchemaOptionsValidator()` registers a singleton `IValidateOptions` (`ZodSchemaOptionsValidator`) that resolves `IZodSchemaFactory` from DI and validates `T` when options are instantiated. Types without a registered validator pass through untouched (`ValidateOptionsResult.Success`). + +The source generator also auto-generates `IValidateOptions` validators for types whose names end in configurable suffixes — see [Source Generator](Source-Generator.md). + +## Example: consuming a factory + +```csharp +public sealed class OrderService(IZodSchemaFactory factory) +{ + public void ValidateProduct(Product product) + { + var result = factory.Validate(product); // ValidationResult + } +} +``` \ No newline at end of file diff --git a/docs/wiki/Fluent-Schema-API.md b/docs/wiki/Fluent-Schema-API.md new file mode 100644 index 0000000..e8d49d0 --- /dev/null +++ b/docs/wiki/Fluent-Schema-API.md @@ -0,0 +1,53 @@ +# Fluent Schema API + +## The `Z` factory + +`public static class Z` (namespace `ZodSharp`) is the entry point for creating schemas. + +| Method | Signature | Returns | +|---|---|---| +| `String()` | `Z.String()` | `ZodString` | +| `Number()` | `Z.Number()` | `ZodNumber` | +| `Boolean()` | `Z.Boolean()` | `ZodBoolean` | +| `Null()` | `Z.Null()` | `ZodNull` | +| `Array` | `Z.Array(IZodSchema elementSchema)` | `ZodArray` | +| `Optional` | `Z.Optional(IZodSchema schema)` — `T : class` | `ZodOptional` | +| `Nullable` | `Z.Nullable(IZodSchema schema)` — `T : struct` | `ZodNullable` | +| `Object()` | `Z.Object()` | `ZodObjectBuilder` | +| `Union` (untyped) | `Z.Union(params IZodSchema[] options)` | `ZodUnion` | +| `Union` (typed) | `Z.Union(IZodSchema, IZodSchema)` | `ZodTypedUnion` | +| `Intersection` | `Z.Intersection(IZodSchema left, IZodSchema right)` | `ZodIntersection` | +| `Literal` | `Z.Literal(T value)` — `T : IEquatable` | `ZodLiteral` | +| `Lazy` | `Z.Lazy(Func> schemaGetter)` | `ZodLazy` | +| `DiscriminatedUnion` | `Z.DiscriminatedUnion(string discriminator)` | `ZodDiscriminatedUnionBuilder` | +| `Enum` (string values) | `Z.Enum(params string[] values)` | `ZodEnum` | +| `Enum` (native) | `Z.Enum()` — `TEnum : struct, Enum` | `ZodNativeEnum` | +| `Record` | `Z.Record(IZodSchema valueSchema)` | `ZodRecord` | +| `Tuple` | `Z.Tuple(IZodSchema, IZodSchema)` | `ZodTuple` | +| `Tuple` | `Z.Tuple(IZodSchema, IZodSchema, IZodSchema)` | `ZodTuple` | +| `ToJsonSchema` | `Z.ToJsonSchema(IZodSchema schema, ToJsonSchemaOptions? options = null)` | `JsonSchemaDefinition` | + +> [!NOTE] +> `Enum` and `Union` are overloaded: `Enum(params string[])` vs `Enum()`, and `Union(params ...)` vs `Union(...)`. `ToJsonSchema` lives in the core package; `FromJsonSchema` is an extension on `Z` provided by the JSON integration packages. + +## Schema type reference + +Each schema type has its own page: + +- [String Validation](String-Validation.md) — `ZodString`. +- [Number Validation](Number-Validation.md) — `ZodNumber`. +- [Object Validation](Object-Validation.md) — `ZodObject`, `ZodObjectBuilder`. +- [Arrays and Other Schemas](Arrays-and-Other-Schemas.md) — `ZodArray`, `ZodBoolean`, `ZodNull`, `ZodEnum`, `ZodNativeEnum`, `ZodLiteral`, `ZodRecord`, `ZodTuple`, `ZodLazy`. +- [Unions and Discriminated Unions](Unions-and-Discriminated-Unions.md) — `ZodUnion`, `ZodTypedUnion`, `ZodDiscriminatedUnion`. +- [Composition and Transforms](Composition-and-Transforms.md) — `ZodTransform`, `ZodRefinement`, `ZodSuperRefinement`, `ZodPipe`, `ZodCatch`, `ZodDefault`, `ZodPrefault`, `ZodIntersection`. +- [Compiled Validators and Caching](Compiled-Validators-and-Caching.md) — `CompiledValidator`, `SchemaCache`. + +## The interfaces + +- `IZodSchema` — `Validate` / `ValidateAsync`; `IZodSchema` is the convenience form where input equals output. +- `IZodSchemaValidator` (marker) and `IZodSchemaValidator` — the DI-facing adapter surface (see [Dependency Injection](Dependency-Injection.md)). +- `IValidationRule` — the rule contract implemented by every struct rule. + +## Convenience composition on any schema + +Because composition is implemented on the base `ZodType`, every schema can chain `.Describe(...)`, `.Transform(...)`, `.Refine(...)`, `.SuperRefine(...)`, `.Pipe(...)`, `.Catch(...)`, `.Prefault(...)`, `.Default(...)`, `.And(...)`, and `.Or(...)`. See [Composition and Transforms](Composition-and-Transforms.md). \ No newline at end of file diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md new file mode 100644 index 0000000..dbd5eef --- /dev/null +++ b/docs/wiki/Getting-Started.md @@ -0,0 +1,131 @@ +# Getting Started + +## Install + +```bash +dotnet add package Purview.ZodSharp +``` + +Add the integration packages you need: + +```bash +# System.Text.Json integration + JSON Schema import +dotnet add package Purview.ZodSharp.SystemTextJson + +# Newtonsoft.Json integration + JSON Schema import +dotnet add package Purview.ZodSharp.NewtonsoftJson + +# ASP.NET Core ProblemDetails integration +dotnet add package Purview.ZodSharp.AspNetCore +``` + +- `Purview.ZodSharp` — core library, source generator, and JSON Schema **export**. +- `Purview.ZodSharp.SystemTextJson` — System.Text.Json deserialize-and-validate, validating converters, and JSON Schema **import**. +- `Purview.ZodSharp.NewtonsoftJson` — Newtonsoft.Json deserialize-and-validate, validating converters, and JSON Schema **import**. +- `Purview.ZodSharp.AspNetCore` — failed validation results converted to standard `ProblemDetails` / `HttpValidationProblemDetails` payloads. + +> [!TIP] +> JSON Schema import (`Z.FromJsonSchema`) is provided by whichever JSON integration package you reference, so pick one. Export (`Z.ToJsonSchema`) lives in the core package. + +## First schema + +```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}"); +``` + +## Validate, SafeParse, and Parse + +- `Validate` returns a `ValidationResult` — no exceptions. +- `SafeParse` is an alias of `Validate`. +- `Parse` throws `ZodException` on failure. + +```csharp +var value = nameSchema.Parse("AB"); // throws ZodException + +var result = nameSchema.SafeParse("AB"); // non-throwing +if (!result.IsSuccess) +{ + foreach (var error in result.Errors) + Console.WriteLine($" - {string.Join(".", error.Path)}: {error.Message}"); +} +``` + +## Object validation + +```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(); + +var userData = new Dictionary +{ + { "name", "John Doe" }, + { "age", 30.0 }, + { "email", "john@example.com" } +}; + +var result = userSchema.Validate(userData); +``` + +## Source-generated validators + +Mark a class, struct, or record with `[ZodSchema]` and a zero-allocation static 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 +``` + +See the [Source Generator](Source-Generator.md) and [Source Generator DataAnnotations](Source-Generator-DataAnnotations.md) pages for the full feature set. + +## JSON integration + +```csharp +// System.Text.Json or Newtonsoft.Json +var json = """{ "name": "John", "age": 30 }"""; +var result = userSchema.DeserializeAndValidate(json); +``` + +```csharp +// JSON Schema export (core) +var jsonSchema = Z.ToJsonSchema(userSchema, new ToJsonSchemaOptions { Title = "User" }); + +// JSON Schema import (requires an integration package) +var imported = Z.FromJsonSchema(jsonSchemaString); +``` + +See [System.Text.Json Integration](SystemTextJson-Integration.md), [Newtonsoft.Json Integration](NewtonsoftJson-Integration.md), [JSON Schema Export](JsonSchema-Export.md), and [JSON Schema Import](JsonSchema-Import.md). + +## Next pages + +- [Core Concepts](Core-Concepts.md) +- [Fluent Schema API](Fluent-Schema-API.md) +- [Source Generator](Source-Generator.md) +- [Dependency Injection](Dependency-Injection.md) +- [Cross-Platform Interop](Cross-Platform-Interop.md) +- [Performance](Performance.md) \ No newline at end of file diff --git a/docs/wiki/Guarantees-and-Limitations.md b/docs/wiki/Guarantees-and-Limitations.md new file mode 100644 index 0000000..5e56094 --- /dev/null +++ b/docs/wiki/Guarantees-and-Limitations.md @@ -0,0 +1,61 @@ +# Guarantees and Limitations + +## Guarantees + +- **Zero-allocation on valid inputs.** Primitives, strings, arrays, objects, discriminated unions, and first-option unions validate without allocating when the input is valid. See [Performance](Performance.md). +- **No reflection on hot paths.** The runtime library uses expression trees only in the opt-in `CompiledValidator`; the source generator emits direct typed codegen. +- **Deterministic, reviewable generated code.** The `[ZodSchema]` generator output is stable and de-duplicated; no scope leaks in emitted code. +- **Cross-platform parity.** The C# implementation is exercised against TypeScript/Zod fixtures (see [Cross-Platform Interop](Cross-Platform-Interop.md)). +- **Multi-targeting.** Packages target `net8.0`, `net9.0`, and `net10.0`; the source generator targets `netstandard2.0` so it runs in any compiler host. +- **Immutable, shareable schemas.** Composing returns new schemas; schemas are safe to cache and share across threads. + +## Limitations + +### Attribute flags that are not yet honoured + +`SchemaName`, `GenerateValidateMethod`, and `GenerateParseMethod` on `[ZodSchema]` are parsed but ignored: the schema class is always `{TypeName}Schema` and `Validate`/`Parse` are always emitted. `EnableComposition` and the `IValidateOptions` flags are honoured. + +### Generated size-failure Origin + +The generator reports `Origin = "array"` for both arrays and collections — there is no `"collection"` origin in generated code, even though `ValidationError.Origin` supports it. + +### Async validation is synchronous underneath + +`ValidateAsync` wraps the synchronous `Validate` pipeline in a `ValueTask`. The only genuinely async path is the source generator's custom async validation method (`CustomValidationAsync`), which is awaited after the sync validation. + +### JSON Schema import scope + +`Z.FromJsonSchema` supports **local** `$ref` (`#/...`) references only; external `$ref` targets throw `NotSupportedException`. `FromJsonSchemaOptions` is currently empty (reserved for future options). + +### Referencing both JSON integration packages + +`Purview.ZodSharp.SystemTextJson` and `Purview.ZodSharp.NewtonsoftJson` both declare types with identical full names (`ZodSharp.ZExtensions`, `ZodSharp.JsonSchema.FromJsonSchemaOptions`, `FromJsonSchemaParser`, `JsonSchemaSerializerOptions`). Reference one JSON integration package; referencing both requires `extern alias`. + +### Typed union allocation + +`ZodTypedUnion`/`ZodUnion` allocate while attempting non-matching options and on failure. Discriminated unions dispatch directly and stay zero-allocation. + +### Rule errors + +Rules evaluated by the base `Validate` pipeline produce `validation_failed` errors with an empty path. Structured `too_small`/`too_big` issues (with `Minimum`/`Maximum`/`Inclusive`) come from the array schema and the source generator's size validators. + +### String transforms allocate + +`ToLower`, `ToUpper`, and `Trim` produce new strings on every validation (transform outputs are new strings by nature). + +### `IStringValidationRule` + +The span-based `IStringValidationRule` interface is declared but not implemented by any shipped rule struct; span validation is available through `ZodString.ValidateSpan`. + +### Number semantics + +`ZodNumber` operates on `double`. `Int()`, `Safe()`, and `Finite()` are validation rules, not conversions; `.Int()` rejects fractional values rather than rounding them. `MultipleOf` uses a tolerance-based comparison and rejects a zero divisor. + +### Enum semantics + +`ZodEnum` (string values) and `ZodNativeEnum` validate against defined members; they do not parse or convert values. + +## Contract vs. underlying libraries + +- `System.ComponentModel.DataAnnotations` semantics are honoured where documented — e.g. `[Length]` treats `null` as valid unless `[Required]` is present; `[RegularExpression]` runs only on non-empty strings. +- Validation failure payloads map to `ProblemDetails`/`HttpValidationProblemDetails` in the ASP.NET Core package (see [ASP.NET Core Integration](AspNetCore-Integration.md)). \ No newline at end of file diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md new file mode 100644 index 0000000..13ddbd8 --- /dev/null +++ b/docs/wiki/Home.md @@ -0,0 +1,58 @@ +# ZodSharp Wiki + +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, ships a fluent API that mirrors Zod, exports and imports JSON Schema, and includes a compile-time source generator for maximum performance. + +This wiki is the project documentation hub for the core API, source generator, JSON integration packages, and the cross-platform TypeScript tooling. It is a fork of [guinhx/ZodSharp](https://github.com/guinhx/ZodSharp), maintained under the `Purview.*` package IDs. + +## Start here + +- [Getting Started](Getting-Started.md) +- [Core Concepts](Core-Concepts.md) +- [Fluent Schema API](Fluent-Schema-API.md) +- [Source Generator](Source-Generator.md) +- [Guarantees and Limitations](Guarantees-and-Limitations.md) +- [Performance](Performance.md) +- [Contributing](Contributing.md) + +## Core validation + +- [String Validation](String-Validation.md) +- [Number Validation](Number-Validation.md) +- [Object Validation](Object-Validation.md) +- [Arrays and Other Schemas](Arrays-and-Other-Schemas.md) +- [Unions and Discriminated Unions](Unions-and-Discriminated-Unions.md) +- [Composition and Transforms](Composition-and-Transforms.md) +- [Compiled Validators and Caching](Compiled-Validators-and-Caching.md) +- [Dependency Injection](Dependency-Injection.md) + +## JSON integration + +- [JSON Schema Export](JsonSchema-Export.md) +- [JSON Schema Import](JsonSchema-Import.md) +- [System.Text.Json Integration](SystemTextJson-Integration.md) +- [Newtonsoft.Json Integration](NewtonsoftJson-Integration.md) +- [ASP.NET Core Integration](AspNetCore-Integration.md) + +## Source generator + +- [Source Generator](Source-Generator.md) +- [Source Generator DataAnnotations](Source-Generator-DataAnnotations.md) +- [Source Generator Diagnostics](Source-Generator-Diagnostics.md) + +## Cross-platform and workflow + +- [Cross-Platform Interop](Cross-Platform-Interop.md) +- [Performance](Performance.md) +- [Release Flow](Release-Flow.md) +- [Contributing](Contributing.md) + +## Feature highlights + +- **Zero-allocation validation** — validation rules are `readonly record struct`s and hot paths use `Span`; every valid input path validates without allocating. +- **Fluent API** — `Z.String().Min(3).Max(50).Email()`, composable objects, arrays, unions, tuples, records, discriminators, and more. +- **Structured issues** — failures carry machine-readable `Code`, `Path`, `Origin`, `Minimum`/`Maximum`, and `Inclusive` metadata in addition to a human message. +- **JSON Schema interoperability** — export via `Z.ToJsonSchema` (core package) and import via `Z.FromJsonSchema` (in either JSON integration package), enabling cross-language reuse with TypeScript/Zod. +- **Compile-time source generation** — the `[ZodSchema]` attribute turns a class, struct, or record into a zero-allocation static validator, honouring DataAnnotations attributes such as `[Required]`, `[Length]`, `[Range]`, and `[EmailAddress]`. +- **Integration packages** — `Purview.ZodSharp.SystemTextJson`, `Purview.ZodSharp.NewtonsoftJson`, and `Purview.ZodSharp.AspNetCore` (ProblemDetails). +- **Cross-platform tests** — a shared TypeScript/Zod fixture set is generated into the repo and asserted against from both the C# test suite and a vitest suite. +- **Multi-target** — packages target `net8.0`, `net9.0`, and `net10.0`; the source generator targets `netstandard2.0` so it runs in any compiler host. \ No newline at end of file diff --git a/docs/wiki/JsonSchema-Export.md b/docs/wiki/JsonSchema-Export.md new file mode 100644 index 0000000..dd5e53e --- /dev/null +++ b/docs/wiki/JsonSchema-Export.md @@ -0,0 +1,70 @@ +# JSON Schema Export + +Export a ZodSharp schema to a JSON Schema (Draft 2020-12) definition with `Z.ToJsonSchema`, which lives in the core `Purview.ZodSharp` package. + +```csharp +using ZodSharp; + +var userSchema = Z.Object() + .Field("name", Z.String().Min(3)) + .Field("email", Z.String().Email()) + .Field("age", Z.Number().Min(0).Int()) + .Build(); + +var jsonSchema = Z.ToJsonSchema>(userSchema, new ToJsonSchemaOptions +{ + Title = "User", + Id = "https://example.com/schemas/user.json" +}); +``` + +## ToJsonSchemaOptions + +| Property | Default | Purpose | +|---|---|---| +| `IncludeSchema` | `true` | emit `$schema: "https://json-schema.org/draft/2020-12/schema"` | +| `Id` | `null` | sets `$id` | +| `Title` | `null` | sets `title` | + +## JsonSchemaDefinition + +`Z.ToJsonSchema` returns `JsonSchemaDefinition` (namespace `ZodSharp.JsonSchema`), a mutable POCO mirroring the JSON Schema keywords: + +- **Identity**: `Schema` (`$schema`), `Id` (`$id`), `Ref` (`$ref`). +- **Type/title**: `Type`, `Title`, `Description`, `Default`, `Format`. +- **String**: `MinLength`, `MaxLength`, `Pattern`. +- **Number**: `Minimum`, `Maximum`, `ExclusiveMinimum`, `ExclusiveMaximum`, `MultipleOf`. +- **Array**: `Items`, `MinItems`, `MaxItems`, `UniqueItems`. +- **Object**: `Properties`, `Required`, `AdditionalProperties`. +- **Composition**: `AnyOf`, `OneOf`, `AllOf`. +- **Enum/const**: `Enum`, `Const`. +- **Definitions**: `Defs` (2020-12) and `Definitions` (draft-07). +- **Metadata**: `Deprecated`, `ReadOnly`, `WriteOnly`, `Examples`, `Nullable` (OpenAPI 3.0). + +## Supported schema types + +`ToJsonSchemaConverter` handles `ZodString`, `ZodNumber`, `ZodBoolean`, `ZodNull`, `ZodObject`, `ZodOptional`, `ZodUnion`, `ZodArray`, `ZodLiteral`, `ZodNullable`, and `ZodLazy`. + +- Objects emit `additionalProperties: false`. +- Literals emit `const` (and `type`). +- Lazy/recursive schemas emit `$ref` entries under `$defs` (e.g. `#/$defs/__lazyN`). + +## Serialize the definition + +Pick the JSON serializer that matches the integration package you referenced: + +```csharp +// System.Text.Json (Purview.ZodSharp.SystemTextJson) +using ZodSharp.JsonSchema; +var json = System.Text.Json.JsonSerializer.Serialize(jsonSchema, JsonSchemaSerializerOptions.Default); + +// Newtonsoft.Json (Purview.ZodSharp.NewtonsoftJson) +using ZodSharp.JsonSchema; +var json = JsonConvert.SerializeObject(jsonSchema, JsonSchemaSerializerOptions.Default); +``` + +`JsonSchemaSerializerOptions.Default` (camelCase, ignore nulls, indented) and `.Reading` (camelCase, ignore nulls) are provided by each integration package. + +## Round-trip + +Import the exported definition back into ZodSharp with `Z.FromJsonSchema` from an integration package — see [JSON Schema Import](JsonSchema-Import.md) and the round-trip example in the example app (`JsonSchemaExamples`). \ No newline at end of file diff --git a/docs/wiki/JsonSchema-Import.md b/docs/wiki/JsonSchema-Import.md new file mode 100644 index 0000000..2ff526f --- /dev/null +++ b/docs/wiki/JsonSchema-Import.md @@ -0,0 +1,67 @@ +# JSON Schema Import + +Import a JSON Schema into a ZodSharp schema with `Z.FromJsonSchema`. This API is provided by the JSON integration packages — reference either `Purview.ZodSharp.SystemTextJson` or `Purview.ZodSharp.NewtonsoftJson` (both expose the same surface). + +```csharp +using ZodSharp; + +var jsonSchemaString = """ + { + "type": "object", + "properties": { + "name": { "type": "string", "minLength": 3 }, + "email": { "type": "string", "format": "email" } + }, + "required": ["name", "email"] + } + """; + +var userSchema = Z.FromJsonSchema(jsonSchemaString); +var result = userSchema.Validate(userData); +``` + +## Overloads + +| Signature | Notes | +|---|---| +| `IZodSchema FromJsonSchema(string jsonSchema, FromJsonSchemaOptions? options = null)` | parses the JSON string into a `JsonSchemaDefinition`, then into a schema | +| `IZodSchema FromJsonSchema(JsonSchemaDefinition schema, FromJsonSchemaOptions? options = null)` | import from an already-deserialized definition | + +`FromJsonSchemaOptions` is currently an empty placeholder reserved for future options. + +> [!NOTE] +> `Z.FromJsonSchema` is implemented as a C# 14 extension member on `Z`, so it only exists when a JSON integration package is referenced. `Z.ToJsonSchema` is a real static member on `Z` in the core package. + +## Supported keywords + +`FromJsonSchemaParser` (namespace `ZodSharp.JsonSchema`) maps: + +- `type` — `string` / `number` / `integer` / `boolean` / `null` / `object` / `array`. +- `enum` → `ZodUnion` of literals (a single member becomes a literal); `const` → literal. +- `anyOf` / `oneOf` → `ZodUnion`; `allOf` → first schema. +- String constraints — `minLength`, `maxLength`, `pattern`, and `format` (`email`, `uri`, `uuid`). +- Numeric constraints — `minimum`, `maximum`, `multipleOf`; `integer` additionally applies `.Int()`. +- Objects — `required` and optional fields via `Z.Object().Field(...)`. +- Arrays — `items`, `minItems`, `maxItems`. + +## Limitations + +- `$ref` is supported only for **local** references (`#/...`); external `$ref` targets throw `NotSupportedException`. +- The options type is currently empty; behaviour is fixed by the supported keyword set above. + +## Cross-platform reuse + +Export a TypeScript/Zod schema to JSON Schema (Zod v4+ `z.toJSONSchema`) and import it on the backend: + +```typescript +import { z } from "zod"; +const UserSchema = z.object({ username: z.string().min(3), email: z.string().email() }); +const jsonSchema = z.toJSONSchema(UserSchema); +``` + +```csharp +var userSchema = Z.FromJsonSchema(jsonSchemaString); +var result = userSchema.Validate(incomingData); +``` + +See [Cross-Platform Interop](Cross-Platform-Interop.md) for the repository's fixture-based verification of this loop. \ No newline at end of file diff --git a/docs/wiki/NewtonsoftJson-Integration.md b/docs/wiki/NewtonsoftJson-Integration.md new file mode 100644 index 0000000..b170ccb --- /dev/null +++ b/docs/wiki/NewtonsoftJson-Integration.md @@ -0,0 +1,86 @@ +# Newtonsoft.Json Integration + +The `Purview.ZodSharp.NewtonsoftJson` package adds Newtonsoft.Json deserialize-and-validate, validating converters, and JSON Schema import to the core library. All extension methods live in the `ZodSharp` namespace. + +## Install + +```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 and `JToken` overloads: + +```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); +``` + +## Validate and serialize + +```csharp +var result = userSchema.ValidateAndSerialize(user); // ValidationResult +var result2 = await userSchema.ValidateAndSerializeAsync(user, stream, formatting: Formatting.Indented); +``` + +## Validating converter + +```csharp +var converter = userSchema.CreateValidatingConverter(); +var value = JsonConvert.DeserializeObject(json, converter); +``` + +`CreateValidatingConverter()` returns a non-generic `Newtonsoft.Json.JsonConverter`. Invalid JSON throws `JsonSerializationException` with a `"Validation failed: ..."` message. The converter clones the `JsonSerializer` (without itself) to avoid recursion. + +## API surface + +| Member | Signature | +|---|---| +| `DeserializeAndValidate` | `ValidationResult DeserializeAndValidate(this IZodSchema schema, string json, JsonSerializerSettings? settings = null)` | +| `DeserializeAndValidate` | `ValidationResult DeserializeAndValidate(this IZodSchema schema, JToken token, JsonSerializer? serializer = null)` | +| `DeserializeAndValidateAsync` | `Task> DeserializeAndValidateAsync(this IZodSchema schema, Stream jsonStream, JsonSerializerSettings? settings = null, CancellationToken cancellationToken = default)` | +| `ValidateAndSerialize` | `ValidationResult ValidateAndSerialize(this IZodSchema schema, T value, JsonSerializerSettings? settings = null, Formatting formatting = Formatting.None)` | +| `ValidateAndSerializeAsync` | `Task> ValidateAndSerializeAsync(this IZodSchema schema, T value, Stream output, JsonSerializerSettings? settings = null, Formatting formatting = Formatting.Indented, CancellationToken cancellationToken = default)` | +| `CreateValidatingConverter` | `JsonConverter CreateValidatingConverter(this IZodSchema schema)` | + +## Failure codes + +Deserialize/validation failures produce `ValidationError` entries with codes `deserialization_failed` and `json_error` in addition to the schema's own codes. + +## JSON Schema import + +`Z.FromJsonSchema` is available with this package referenced; see [JSON Schema Import](JsonSchema-Import.md). + +## System.Text.Json vs Newtonsoft.Json + +| Aspect | SystemTextJson | NewtonsoftJson | +|---|---|---| +| Async result type | `ValueTask<...>` | `Task<...>` | +| Options parameter | `System.Text.Json.JsonSerializerOptions` | `Newtonsoft.Json.JsonSerializerSettings` | +| Converter return | generic `JsonConverter` | non-generic `JsonConverter` | +| `JToken` overload | no | yes | +| Formatting control | via `JsonSerializerOptions` | explicit `Newtonsoft.Json.Formatting` argument | +| Invalid-data exception | `System.Text.Json.JsonException` | `JsonSerializationException` | +| JSON plumbing | `JsonElement` | `JToken`/`JObject`/`JArray` | + +> [!WARNING] +> Both packages declare types with identical full names (`ZodSharp.ZExtensions`, `ZodSharp.JsonSchema.FromJsonSchemaOptions`, `ZodSharp.JsonSchema.FromJsonSchemaParser`, `ZodSharp.JsonSchema.JsonSchemaSerializerOptions`). Referencing both packages in one project creates type ambiguity unless `extern alias` is used — reference one JSON integration package. \ No newline at end of file diff --git a/docs/wiki/Number-Validation.md b/docs/wiki/Number-Validation.md new file mode 100644 index 0000000..ba74389 --- /dev/null +++ b/docs/wiki/Number-Validation.md @@ -0,0 +1,41 @@ +# Number Validation + +`ZodNumber` (namespace `ZodSharp.Schemas`) validates `double` values. `ParseInternal` rejects `double.NaN` with an `invalid_type` error (`"Expected number, but got NaN"`); on success the accumulated rules run. + +```csharp +using ZodSharp; + +var schema = Z.Number().Min(0).Max(120).Int(); +var result = schema.Validate(30.0); +``` + +## Methods + +| Method | Signature | Rule added | +|---|---|---| +| `Min` | `Min(double minValue)` | `MinValueRule` — `Value must be at least ...` | +| `Max` | `Max(double maxValue)` | `MaxValueRule` | +| `Int` | `Int()` | `IntRule` — `value == Math.Truncate(value)` | +| `Positive` | `Positive()` | `MinValueRule(0.0)` | +| `Negative` | `Negative()` | `MaxValueRule(0.0)` | +| `MultipleOf` | `MultipleOf(double divisor, string? message)` | `MultipleOfRule` — throws `ArgumentException` for a zero divisor; tolerance-based | +| `Finite` | `Finite(string? message)` | `FiniteRule` — `double.IsFinite` | +| `Safe` | `Safe(string? message)` | `SafeIntegerRule` — integer within `int.MinValue`..`int.MaxValue` | + +## Examples + +```csharp +var positive = Z.Number().Positive(); +var negative = Z.Number().Negative(); + +var multipleOf = Z.Number().MultipleOf(10); // multiples of 10 +var finite = Z.Number().Finite(); // rejects Infinity / NaN +var safe = Z.Number().Safe(); // safe integer range +var whole = Z.Number().Int(); // no fractional part + +var age = Z.Number().Min(0).Max(120).Int().Validate(25.0); +``` + +## Numeric coercion + +When a `Z.Number()` is used as an object field or union option, boxed values are coerced via `IConvertible` (invariant culture) — for example a `long` from a `Dictionary` validates against a `Z.Number()` field. Non-numeric values fail with `invalid_type`. \ No newline at end of file diff --git a/docs/wiki/Object-Validation.md b/docs/wiki/Object-Validation.md new file mode 100644 index 0000000..666a31b --- /dev/null +++ b/docs/wiki/Object-Validation.md @@ -0,0 +1,79 @@ +# Object Validation + +`ZodObject` (namespace `ZodSharp.Schemas`) validates `Dictionary` values. Build schemas with the `ZodObjectBuilder` returned by `Z.Object()`. + +```csharp +using ZodSharp; + +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(); + +var result = userSchema.Validate(new Dictionary +{ + { "name", "John Doe" }, + { "age", 30.0 }, + { "email", "john@example.com" } +}); +``` + +## Behaviour + +- `null` input → `invalid_type` (`"Expected object, but got null"`). +- Missing fields are allowed only when the key is optional (`Partial`/`Required`/`.Optional` semantics) or the field schema is itself optional (`IOptionalSchema.IsOptional`, e.g. `Z.Optional(...)`); otherwise `missing_field` with path `[key]`. +- When a missing field's schema `ProvidesValueOnMissing` (e.g. `Z.Default(...)`), the produced value is injected into the output. +- Field values are validated against their schema; failures get the field name prepended to the error path. Changed/coerced values trigger a rebuild of the output dictionary. +- Unknown keys are handled by the object's `UnknownKeyPolicy` or `CatchallSchema` (below). + +## Unknown key policies + +`UnknownKeyPolicy` is an enum with three values. `Strip` is the default. + +| Policy | Behaviour | +|---|---| +| `Strip` (default) | Unknown keys are dropped from the output. | +| `Passthrough` | Unknown keys are kept as-is. | +| `Strict` | Unknown keys fail with `unrecognized_key` and path `[key]`. | + +```csharp +var strict = Z.Object().Field("name", Z.String()).Build().Strict(); +var permissive = Z.Object().Field("name", Z.String()).Build().Passthrough(); +``` + +## Catchall + +`Catchall(IZodSchema schema)` validates every unknown key against the schema and includes the validated value in the output. The schema argument must not be `null`. + +```csharp +var schema = Z.Object() + .Field("name", Z.String()) + .Catchall(Z.Number()) + .Build(); +``` + +## Fluent methods + +These return a **new** `ZodObject` instance: + +| Method | Behaviour | +|---|---| +| `Extend(string key, IZodSchema schema)` | add or replace a field | +| `Merge(ZodObject other)` | other's shape overrides; adopts other's `UnknownKeyPolicy` + `CatchallSchema`; optionality per contributing object | +| `Pick(params string[] keys)` | keep only the given keys | +| `Omit(params string[] keys)` | remove the given keys | +| `Partial()` | every shape key optional | +| `Required()` | no optional keys; all shape keys required | +| `Passthrough()` | `UnknownKeyPolicy.Passthrough` | +| `Strict()` | `UnknownKeyPolicy.Strict` | +| `Strip()` | `UnknownKeyPolicy.Strip` | +| `Catchall(IZodSchema schema)` | validate unknown keys against a schema | + +## Exposed shape + +`ZodObject` exposes `Shape`, `UnknownKeyPolicy`, `OptionalKeys`, `RequiredKeys`, and `CatchallSchema` as read-only properties, so metadata is inspectable (used by the JSON Schema exporter). + +## Builder + +`ZodObjectBuilder` validates its arguments: a null/whitespace field name or a null schema throws `ArgumentNullException`. Typed fields are wrapped so boxed values coerce correctly (see [Core Concepts](Core-Concepts.md)). \ No newline at end of file diff --git a/docs/wiki/Performance.md b/docs/wiki/Performance.md new file mode 100644 index 0000000..58a40c9 --- /dev/null +++ b/docs/wiki/Performance.md @@ -0,0 +1,109 @@ +# Performance + +ZodSharp is designed for maximum performance: validation rules are `readonly record struct`s, hot paths use `Span`, and the source generator emits direct typed codegen with no reflection. The committed BenchmarkDotNet suite measures every scenario. + +## Running the benchmarks + +```bash +# All suites +dotnet run --project src/src/Benchmarks/Benchmarks.csproj -c Release +# or +just perf-tests + +# A specific suite (the `--` passes the filter to BenchmarkDotNet) +dotnet run --project src/src/Benchmarks/Benchmarks.csproj -c Release -- --filter "*ObjectPerformanceTests*" +``` + +Results are written to `BenchmarkDotNet.Artifacts/` (HTML, Markdown, logs) in the project directory. Use `-c Release`; the suite uses `[MemoryDiagnoser]` and a `[SimpleJob]` profile. + +## Measurement environment + +- BenchmarkDotNet 0.15.8, .NET 10.0.12, Windows 11 (10.0.28020.2991). +- 13th Gen Intel Core i9-13900KF 3.00 GHz (24 physical / 32 logical cores), X64 RyuJIT x86-64-v3. + +Numbers are indicative; re-run on your own hardware for local planning. + +## Core validation (`BasicPerformanceTests`) + +| Scenario | Mean | Allocated | +|---|---|---| +| ValidateBoolean | 2.170 ns | 0 B | +| ValidateNumber | 10.702 ns | 0 B | +| ValidateString | 41.627 ns | 0 B | +| ValidateStringArray | 57.031 ns | 0 B | +| ValidateStringWithMultipleRules | 77.313 ns | 0 B | +| ValidateNumberWithMultipleRules | 16.427 ns | 0 B | + +## Objects (`ObjectPerformanceTests`) + +| Scenario | Mean | Allocated | +|---|---|---| +| ValidateSimpleObject (2 fields) | 89.78 ns | 0 B | +| ValidateMediumObject (6 fields) | 340.18 ns | 0 B | +| ValidateComplexObject (13 fields, nested) | 795.69 ns | 0 B | +| ValidateComplexObjectInvalid | 1,152.80 ns | 1,960 B | + +## Arrays (`ArrayPerformanceTests`) + +| Scenario | Mean | Allocated | +|---|---|---| +| ValidateSmallArray | 124.4 ns | — | +| ValidateLargeArray (1000 items) | 11,682.3 ns | — | +| ValidateMediumArray (100 items) | 4,585.3 ns | — | +| ValidateNumberArray | 12,797.3 ns | — | +| ValidateLargeArrayWithComplexSchema | 8,011.8 ns | — | +| ValidateLargeArrayInvalid | 11,206.7 ns | 904 B | + +## Heavy scenarios (`HeavyPerformanceTests`) + +| Scenario | Mean | +|---|---| +| ValidateDeepNestedObject (4 levels) | 198.5 ns | +| ValidateWideObject (50 fields) | 2,217.2 ns | +| ValidateNestedArray | 177.7 ns | +| ValidateStringWithManyRefinements | 114.7 ns | +| ValidateLargeObjectWithArrays | 19,578.0 ns | + +## Transforms (`TransformPerformanceTests`) + +| Scenario | Mean | Allocated | +|---|---|---| +| TransformToLower | 22.26 ns | 48 B | +| TransformToUpper | 28.39 ns | 48 B | +| TransformTrim | 29.10 ns | 48 B | +| TransformChained | 40.71 ns | 96 B | +| TransformWithValidation | 78.79 ns | 112 B | + +## Unions (`UnionPerformanceTests`) + +| Scenario | Mean | Allocated | +|---|---|---| +| ValidateUnion_String (first option) | 48.70 ns | 0 B | +| ValidateUnion_Number (second option) | 69.38 ns | 592 B | +| ValidateUnion_Boolean (third option) | 99.71 ns | 792 B | +| ValidateDiscriminatedUnion_FirstOption | 169.33 ns | 0 B | +| ValidateDiscriminatedUnion_SecondOption | 171.90 ns | 0 B | +| ValidateUnion_Invalid | 195.87 ns | 1,360 B | + +> [!NOTE] +> Union validations allocate on the failure path and while attempting non-matching options — the string option is free, but matching a later option allocates the error collection from the earlier attempts. Discriminated unions dispatch directly and remain zero-allocation. + +## Memory (`MemoryPerformanceTests`) + +All valid-input paths are zero-allocation: + +| Scenario | Mean | Ratio | +|---|---|---| +| ValidateString_Allocations (baseline) | 45.82 ns | 1.00 | +| ValidateObject_Allocations | 94.28 ns | 2.06 | +| ValidateArray_Allocations | 1,180.24 ns | 25.82 | + +## Optimizations that make it fast + +1. **Struct-based rules** — every rule is a `readonly record struct` implementing `IValidationRule`, so there is no per-validation object allocation. +2. **Zero-allocation helpers** — `Span`/`ReadOnlySpan` string validation (`ValidateSpan`) and `ArrayPool`-backed helpers. +3. **Compiled validators** — `CompiledValidator.Compile` removes interface dispatch (see [Compiled Validators and Caching](Compiled-Validators-and-Caching.md)). +4. **Source generation** — `[ZodSchema]` emits direct property access and typed equality checks with no reflection (see [Source Generator](Source-Generator.md)). +5. **Fluent composition** — schemas are immutable and shareable, so `SchemaCache` avoids repeated construction (see [Compiled Validators and Caching](Compiled-Validators-and-Caching.md)). + +The only allocations on a successful validation are the string transforms (`ToLower`/`ToUpper`/`Trim` produce new strings) and the union non-first-option paths noted above. \ No newline at end of file diff --git a/docs/wiki/Release-Flow.md b/docs/wiki/Release-Flow.md new file mode 100644 index 0000000..8299c47 --- /dev/null +++ b/docs/wiki/Release-Flow.md @@ -0,0 +1,46 @@ +# Release Flow + +ZodSharp releases are driven by the shared [purview-dev/build](https://github.com/purview-dev/build) pipeline through the GitHub Actions workflows in `.github/workflows/`. + +## Versioning + +The package version comes from `package.json` (`version` field). The repo is currently on the `2.0.0-prerelease.*` line. Bump `package.json` to release a new version. + +Package identities are `Purview.ZodSharp.*` (core, SystemTextJson, NewtonsoftJson, AspNetCore). Central package management lives in `Directory.Packages.props`; package versions there are minimum requirements, not exact pins, so the resolved graph can drift. + +## Workflows + +| Workflow | Trigger | Pipeline mode | +|---|---|---| +| `pr.yml` | pull requests against `main` | `purview-build.yml` with `run-pack: true`, `validate-pack: true` | +| `release.yml` | pushes to `main` | `purview-release.yml` with `release-mode: NuGet` | + +Both consume `purview-build.json`: + +- `Build` — solution (`src/ZodSharp.slnx`), test root (`src/tests`), patterns (`*Tests.csproj`), filter (`/*/*/*/*`). +- `PackValidation` — requires symbol packages/files and validates `RequiredContent` per package (per-TFM DLL + XML, analyzer assemblies and `buildTransitive/Purview.ZodSharp.props` for the core package, `README.md` and `purview-logo.png` for every package). +- `Release.Mode` — `None` for PR/local runs. + +## Pipeline commands + +```bash +just pipeline-pr # restore, build, lint, tests, pack, validate pack +just pipeline-build # restore, build, lint (no tests, no release) +just pipeline-tests # pipeline with tests enabled +just pipeline-release # pack, publish, GitHub release (NuGet mode) +just pipeline-local-release # pack + publish to a local NuGet feed +``` + +See the `Justfile` for the full command set (`just build`, `just test`, `just lint-check`, `just lint-fix`, `just pack`, `just perf-tests`). + +## Commit conventions + +Commits must follow [Conventional Commits](https://www.conventionalcommits.org/), enforced by Lefthook + Commitlint (`.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 test`). +- `just lint-check` (CSharpier) reports no formatting changes. +- Packed packages match `purview-build.json` `PackValidation`. +- Generated code is deterministic and reviewable. \ No newline at end of file diff --git a/docs/wiki/Source-Generator-DataAnnotations.md b/docs/wiki/Source-Generator-DataAnnotations.md new file mode 100644 index 0000000..f53f519 --- /dev/null +++ b/docs/wiki/Source-Generator-DataAnnotations.md @@ -0,0 +1,87 @@ +# Source Generator DataAnnotations + +The `[ZodSchema]` generator reads `System.ComponentModel.DataAnnotations` attributes and emits direct, typed codegen — no reflection at runtime. + +## Supported attributes + +| Attribute | Generated behaviour | Failure code | +|---|---|---| +| `[Required]` | nullable property must not be null (strings with `AllowEmptyStrings=false` must be non-empty) | `missing_field` | +| `[Length(min, max)]` | min/max size with `too_small`/`too_big`; applies to strings, arrays (incl. jagged/rectangular), and countable collections | `too_small` / `too_big` | +| `[StringLength(max)]` / `[StringLength(max, MinimumLength=min)]` | string size limits via direct `.Length` | `too_small` / `too_big` | +| `[MinLength(n)]` | only checked when `n > 0` | `too_small` | +| `[MaxLength(n)]` | only checked when `n >= 0` | `too_big` | +| `[Range(...)]` | inclusive (or exclusive) numeric/parsed bounds | `invalid_range` | +| `[RegularExpression(pattern)]` | compiled `Regex` field, checked on non-empty strings | `invalid_string` | +| `[AllowedValues(...)]` | typed equality checks against the allowed set | `invalid_value` | +| `[DeniedValues(...)]` | typed equality checks against the denied set | `invalid_value` | +| `[EmailAddress]` | reuses `ZodSharp.Rules.EmailRule` on non-empty strings | `invalid_string` | +| `[Url]` | reuses `UrlRule` | `invalid_string` | +| `[Phone]` | reuses `PhoneRule` | `invalid_string` | +| `[CreditCard]` | reuses `CreditCardRule` | `invalid_string` | +| `[Base64String]` | reuses `Base64StringRule` | `invalid_string` | +| `[Compare(otherProperty)]` | typed equality between two properties | `mismatch` | +| `[Display(Name=...)]` | not validated; `Name` used as the display name in messages and `{0}` placeholders | — | + +`[Length]` follows DataAnnotations null semantics: `null` is valid unless `[Required]` is also present. + +## Size validators and structured issues + +Size attributes generate direct `Length` or `Count` access when possible: + +- `string` → `.Length`. +- arrays (including rectangular arrays) → `.Length`. +- jagged arrays → outer-array `.Length`. +- countable collections → `.Count`. +- `IEnumerable` / `IEnumerable` → a single counted pass via `CollectionCountHelper.GetCount` (fast paths for `ICollection`, `IReadOnlyCollection`, and non-generic `ICollection`). + +Structured size failures expose the same metadata as the runtime API: + +- `Code`: `too_small` or `too_big`. +- `Origin`: `string` for strings, `array` for arrays and collections. +- `Minimum` / `Maximum`: the inclusive bound. +- `Inclusive`: `true`. +- `Path`: the property path. + +```csharp +[ZodSchema] +public sealed class Basket +{ + [Required] + [Length(2, 5)] + public List? Items { get; set; } +} + +var result = BasketSchema.Validate(new Basket { Items = ["apple"] }); +// result.Errors[0].Code == "too_small" +// result.Errors[0].Minimum == 2 +// result.Errors[0].Origin == "array" +// result.Errors[0].Inclusive == true +``` + +> [!NOTE] +> Today the generator reports `Origin = "array"` for both arrays and collections; there is no `"collection"` origin in generated code. + +## Range + +`[Range]` supports three constructor shapes plus `MinimumIsExclusive`, `MaximumIsExclusive`, `ConvertValueInInvariantCulture`, and `ParseLimitsInInvariantCulture`: + +- `[Range(int, int)]` and `[Range(double, double)]` — literal numeric bounds. +- `[Range(typeof(T), "min", "max")]` — parsed bounds for numeric types and comparable types. + +Comparable range targets include `TimeSpan`, `DateTime`, `DateTimeOffset`, `DateOnly`, `TimeOnly`, and `Version` (which uses `CompareTo`), plus any type implementing `IComparable` with user-defined comparison operators. Bounds are emitted as static typed fields and compared without runtime attribute execution. + +## Error message customization + +Honours `ErrorMessage`, or `ErrorMessageResourceName` + `ErrorMessageResourceType`, with `{0}` (display name), `{1}`, and `{2}` (bound) placeholders formatted via `string.Format(CultureInfo.CurrentCulture, ...)`. Providing only one of the resource name/type pair is reported as ZODSGEN005. + +## Type applicability diagnostics + +Misuse is reported at compile time rather than silently ignored: + +- `[Length]` with `min > max` → ZODSGEN003. +- `[Length]` on an unsupported target (e.g. `decimal`) → ZODSGEN004. +- String-only attributes (`[RegularExpression]`, `[EmailAddress]`, `[Url]`, `[Phone]`, `[CreditCard]`, `[Base64String]`) on non-string targets, `[AllowedValues]`/`[DeniedValues]` on unsupported types, or `[Range]` on unsupported types → ZODSGEN006. +- `[Compare]` referencing an unknown property → ZODSGEN020. + +See [Source Generator Diagnostics](Source-Generator-Diagnostics.md) for the full list. \ No newline at end of file diff --git a/docs/wiki/Source-Generator-Diagnostics.md b/docs/wiki/Source-Generator-Diagnostics.md new file mode 100644 index 0000000..e722ab4 --- /dev/null +++ b/docs/wiki/Source-Generator-Diagnostics.md @@ -0,0 +1,37 @@ +# Source Generator Diagnostics + +The `[ZodSchema]` generator ships an analyzer (category `ZodSharp.SourceGenerator`) that reports configuration and usage problems at compile time. All diagnostics below are errors, enabled by default. + +| ID | Meaning | +|---|---| +| ZODSGEN001 | Unhandled generator exception (`"Source generator failed for {0}: {1}"`) | +| ZODSGEN003 | Invalid `[Length]` configuration (min > max) | +| ZODSGEN004 | Unsupported `[Length]` target | +| ZODSGEN005 | Invalid DataAnnotations error-message resource configuration (name without type, or type without name) | +| ZODSGEN006 | Unsupported DataAnnotations usage (string-only attributes on non-string targets; `[AllowedValues]`/`[DeniedValues]` on unsupported types; `[RegularExpression]` on non-strings; `[Range]` on unsupported types) | +| ZODSGEN007 | Custom/synchronous validation method configured but not found (when a name is explicitly configured) | +| ZODSGEN008 | Custom method return type is not `ValueTask>` | +| ZODSGEN009 | Custom method parameter count is not 2 | +| ZODSGEN010 | First custom method parameter is not the model type | +| ZODSGEN011 | Second custom method parameter is not `CancellationToken` | +| ZODSGEN012 | Custom/synchronous method is generic | +| ZODSGEN013 | Custom method must be static when defined on the model type | +| ZODSGEN014 | Custom method is inaccessible from the generated validator (private/protected) | +| ZODSGEN015 | Ambiguous custom/synchronous method overloads (only when at least two valid candidates exist) | +| ZODSGEN016 | Configured method name is not a valid C# identifier | +| ZODSGEN017 | Custom/synchronous method is abstract | +| ZODSGEN018 | Custom/synchronous method is an unimplemented partial method | +| ZODSGEN019 | Custom/synchronous method uses `ref`/`in`/`out`/`params`/`scoped` parameters | +| ZODSGEN020 | `[Compare]` references an unknown property | +| ZODSGEN021 | `System.ComponentModel.DataAnnotations` reference missing | +| ZODSGEN022 | Synchronous refinement return type is not `IEnumerable` (arrays/derived assignable types accepted) | +| ZODSGEN023 | Synchronous refinement has more than one parameter | +| ZODSGEN024 | Synchronous refinement must be an instance method | +| ZODSGEN025 | Synchronous refinement must be public or internal | +| ZODSGEN026 | Synchronous refinement's single parameter must be `RefineCtx` matching the model | +| ZODSGEN027 | `IValidateOptions` requested but `Microsoft.Extensions.Options` reference is missing | +| ZODSGEN028 | `IValidateOptions` requested on a struct (requires a class) | + +## Suppressing + +Diagnostics can be suppressed per-project or per-site with the standard `#pragma warning disable ZODSGEN006` / `NoWarn` mechanisms. Refer to the analyzer's shipped release notes (`AnalyzerReleases.Shipped.md` / `AnalyzerReleases.Unshipped.md` in the generator project) for the canonical catalog. \ No newline at end of file diff --git a/docs/wiki/Source-Generator.md b/docs/wiki/Source-Generator.md new file mode 100644 index 0000000..c4382f6 --- /dev/null +++ b/docs/wiki/Source-Generator.md @@ -0,0 +1,128 @@ +# Source Generator + +Mark a class, struct, or record with `[ZodSchema]` and the generator emits a static, zero-allocation validator at compile time. The `[ZodSchema]` attribute is generated into the `ZodSharp` namespace by the generator itself (assembly `Purview.ZodSharp.SourceGenerators`), so no extra package is needed beyond `Purview.ZodSharp`. + +```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 ZodException on failure +``` + +## Generated types + +For a `[ZodSchema]` target type `{TypeName}`, the generator emits: + +| Artifact | Shape | +|---|---| +| `{TypeName}Schema` | static partial class — the validator; access mirrors the target (public/internal/private for private nested types); contains `Validate`, `Parse`, and (when composition is enabled) `ApplyAnd`, `ApplyOr`, `ApplyRefine` | +| `{TypeName}SchemaValidator` | `partial class {TypeName}SchemaValidator : IZodSchemaValidator<{TypeName}>` — DI-friendly adapter with `Validate` / `ValidateAsync`; emitted only for the primary schema | +| `{TypeName}Validator` | `sealed partial class {TypeName}Validator : IValidateOptions<{TypeName}>` — emitted only when `IValidateOptions` support is enabled (and the target is a class) | +| `[assembly: ZodSchemaGenerated(typeof({TypeName}))]` | registration marker consumed by `IZodSchemaFactory` assembly scanning; emitted only for primary, non-nested schemas | + +```csharp +// Value-first composition methods (EnableComposition, default true): +var adult = UserSchema.ApplyRefine(user, u => u.Age >= 18, "Must be adult"); +var both = UserSchema.ApplyAnd(user, u => u.Name.Length > 5, "Name too short"); +var either = UserSchema.ApplyOr(user, u => u.Age < 18, "Must be an adult or a minor with consent"); +``` + +## Attribute options + +All options are optional. + +| Property | Default | Purpose | +|---|---|---| +| `SchemaName` | `null` | Reserved — the schema class is always named `{TypeName}Schema`. | +| `GenerateValidateMethod` | `true` | Reserved — `Validate` is always emitted. | +| `GenerateParseMethod` | `true` | Reserved — `Parse` is always emitted. | +| `EnableComposition` | `true` | Emits `ApplyAnd`, `ApplyOr`, `ApplyRefine` value-first composition methods. | +| `CustomValidationMethodName` | `null` | Name of an async custom validation method; default lookup name `CustomValidationAsync`. | +| `RefinementMethodName` | `null` | Name of a synchronous refinement method; default lookup name `Validate` (an instance method on the model). | +| `GenerateIValidateOptions` | `false` | Force `IValidateOptions` generation. | +| `SuppressIValidateOptions` | `false` | Opt out even when auto-detection would enable it. | + +> [!NOTE] +> `SchemaName`, `GenerateValidateMethod`, and `GenerateParseMethod` are parsed by the attribute but not yet honoured by the generator — the class is always `{TypeName}Schema` with `Validate` and `Parse`. + +## Custom async validation + +Declare a partial `{TypeName}SchemaValidator` (or a static method on the model type): + +```csharp +public partial class UserSchemaValidator +{ + public async ValueTask> CustomValidationAsync(User value, CancellationToken ct) + { + await Task.Delay(1, ct); + return ValidationResult.Success(value); + } +} +``` + +Requirements: + +- Signature `ValueTask> Name(T value, CancellationToken ct)`. +- A method declared on the model type must be `static`; a method on the generated `{TypeName}SchemaValidator` partial may be an instance method. +- The generated `ValidateAsync` runs the synchronous `Validate`, then awaits the custom method, and merges the error sets. + +## Synchronous refinement + +Declare an instance method on the model (default name `Validate`) returning `IEnumerable`: + +```csharp +[ZodSchema(RefinementMethodName = "Validate")] +public class Order +{ + public decimal Total { get; set; } + + public IEnumerable Validate() + { + if (Total < 0) + yield return ValidationError.Create("invalid_range", "Total cannot be negative", []); + } +} +``` + +Parameterless or `IEnumerable Validate(RefineCtx ctx)` variants are supported. + +## IValidateOptions support + +Generated options validators are enabled by: + +1. `GenerateIValidateOptions = true` on the attribute, or +2. auto-detection: `GenerateIValidateOptions` unset, target is not a value type, and the type name ends with a configured suffix (default `Options` or `Settings`), or +3. MSBuild override. + +MSBuild switches: + +| Property | Default | Behaviour | +|---|---|---| +| `DisableZodSharpSourceGenerator` | unset | disables the generator entirely when truthy | +| `ZodSharpAutoGenerateOptionsValidators` | `true` | auto-detect `IValidateOptions` (only explicit `false` disables) | +| `ZodSharpAutoGenerateOptionsValidatorSuffixes` | `Options;Settings` | semicolon/comma-separated suffix list | + +## What is validated + +- Properties must be public, non-static, non-indexer. +- A property is included when it carries any DataAnnotations attribute or its type is a source-defined complex type with a nested schema. +- Classes, structs, and records are supported; structs do not receive `IValidateOptions` (ZODSGEN028 if requested). +- Nested complex types are discovered recursively and get their own generated `{TypeName}Schema`, even when the nested type does not itself carry `[ZodSchema]`. +- Nullable properties are null-guarded before value-set/type validation; a nullable target rejects `null` with `invalid_type`. + +See [Source Generator DataAnnotations](Source-Generator-DataAnnotations.md) for the attribute coverage and structured issue shape, and [Source Generator Diagnostics](Source-Generator-Diagnostics.md) for the `ZODSGEN*` diagnostics. \ No newline at end of file diff --git a/docs/wiki/String-Validation.md b/docs/wiki/String-Validation.md new file mode 100644 index 0000000..9443e63 --- /dev/null +++ b/docs/wiki/String-Validation.md @@ -0,0 +1,62 @@ +# String Validation + +`ZodString` (namespace `ZodSharp.Schemas`) validates `string` values. `ParseInternal` rejects `null` with an `invalid_type` error (`"Expected string, but got null"`); on success the accumulated rules run. + +```csharp +using ZodSharp; + +var schema = Z.String().Min(3).Max(50).Email(); +var result = schema.Validate("user@example.com"); +``` + +## Methods + +| Method | Signature | Rule added | +|---|---|---| +| `Min` | `Min(int minLength)` | `MinLengthRule` — `too_small` via `validation_failed` when too short | +| `Max` | `Max(int maxLength)` | `MaxLengthRule` | +| `Length` | `Length(int length)` | exact length (both bounds) | +| `Email` | `Email()` | `EmailRule` — static compiled regex | +| `Regex` | `Regex(Regex pattern, string? message)` / `Regex(string pattern, string? message)` | `RegexRule`; the string overload compiles with a 100 ms timeout | +| `Url` | `Url(string? message)` | `UrlRule` — regex or absolute `http`/`https` URI | +| `Phone` | `Phone(string? message)` | `PhoneRule` — digits plus `() .+-`, at least one digit | +| `CreditCard` | `CreditCard(string? message)` | `CreditCardRule` — Luhn algorithm | +| `Base64String` | `Base64String(string? message)` | `Base64StringRule` — `Convert.FromBase64String` | +| `UUID` | `UUID(string? message)` | `UUIDRule` — `xxxxxxxx-xxxx-...` regex | +| `StartsWith` | `StartsWith(string prefix, string? message)` | `StartsWithRule` — ordinal comparison | +| `EndsWith` | `EndsWith(string suffix, string? message)` | `EndsWithRule` — ordinal comparison | +| `ToLower` | `ToLower()` | wraps a transform (`ToLowerInvariant`), returns a `ZodString` | +| `ToUpper` | `ToUpper()` | wraps a transform (`ToUpperInvariant`) | +| `Trim` | `Trim()` | wraps a transform (`Trim`) | +| `ValidateSpan` | `ValidateSpan(ReadOnlySpan value)` | zero-allocation span validation | + +> [!NOTE] +> `ToLower`, `ToUpper`, and `Trim` produce a new string on every validation — these are the only string validations that allocate on a successful path. + +## Examples + +```csharp +var email = Z.String().Email().Validate("user@example.com"); + +var url = Z.String().Url().Validate("https://example.com"); + +var uuid = Z.String().UUID().Validate("550e8400-e29b-41d4-a716-446655440000"); + +var prefix = Z.String().StartsWith("https://"); +var suffix = Z.String().EndsWith(".com"); + +var exact = Z.String().Length(10); + +var normalized = Z.String().Trim().ToUpper().Validate(" hello "); // "HELLO" + +ReadOnlySpan span = "user@example.com".AsSpan(); +var spanResult = Z.String().Min(3).Max(50).Email().ValidateSpan(span); +``` + +## Error messages + +Rules produce `ValidationError` entries with code `validation_failed` and an empty path. Many methods accept a custom `message` parameter. Rule structs live in `ZodSharp.Rules` and can be reused standalone with `IValidationRule`. + +## Span validation + +`ValidateSpan(ReadOnlySpan value)` avoids string allocations on the validation path. An empty span validates successfully as `""`. \ No newline at end of file diff --git a/docs/wiki/SystemTextJson-Integration.md b/docs/wiki/SystemTextJson-Integration.md new file mode 100644 index 0000000..7ecbb35 --- /dev/null +++ b/docs/wiki/SystemTextJson-Integration.md @@ -0,0 +1,72 @@ +# System.Text.Json Integration + +The `Purview.ZodSharp.SystemTextJson` package adds System.Text.Json deserialize-and-validate, validating converters, and JSON Schema import to the core library. All extension methods live in the `ZodSharp` namespace. + +## Install + +```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); +``` + +## Validate and serialize + +```csharp +var result = userSchema.ValidateAndSerialize(user); // ValidationResult +var result2 = await userSchema.ValidateAndSerializeAsync(user, stream); +``` + +## Validating converter + +```csharp +var converter = userSchema.CreateValidatingConverter(); +var options = new JsonSerializerOptions { Converters = { converter } }; +var value = JsonSerializer.Deserialize(json, options); +``` + +`CreateValidatingConverter()` returns a `System.Text.Json.Serialization.JsonConverter`. When the JSON is invalid, deserialization throws `JsonException` with a `"Validation failed: ..."` message. The converter strips itself from the options it uses internally to avoid recursion. + +## API surface + +| Member | Signature | +|---|---| +| `DeserializeAndValidate` | `ValidationResult DeserializeAndValidate(this IZodSchema schema, string json, JsonSerializerOptions? options = null)` | +| `DeserializeAndValidateAsync` | `ValueTask> DeserializeAndValidateAsync(this IZodSchema schema, Stream jsonStream, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default)` | +| `ValidateAndSerialize` | `ValidationResult ValidateAndSerialize(this IZodSchema schema, T value, JsonSerializerOptions? options = null)` | +| `ValidateAndSerializeAsync` | `ValueTask> ValidateAndSerializeAsync(this IZodSchema schema, T value, Stream output, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default)` | +| `CreateValidatingConverter` | `JsonConverter CreateValidatingConverter(this IZodSchema schema)` | + +## Failure codes + +Deserialize/validation failures produce `ValidationError` entries with codes `deserialization_failed` and `json_error` in addition to the schema's own codes. + +## JSON Schema import + +`Z.FromJsonSchema` is available with this package referenced; see [JSON Schema Import](JsonSchema-Import.md). + +## Comparing with Newtonsoft + +See the API comparison table on the [Newtonsoft.Json Integration](NewtonsoftJson-Integration.md) page for the differences between the two JSON packages. \ No newline at end of file diff --git a/docs/wiki/Unions-and-Discriminated-Unions.md b/docs/wiki/Unions-and-Discriminated-Unions.md new file mode 100644 index 0000000..2455ef5 --- /dev/null +++ b/docs/wiki/Unions-and-Discriminated-Unions.md @@ -0,0 +1,75 @@ +# Unions and Discriminated Unions + +## Untyped union + +`ZodUnion` (namespace `ZodSharp.Schemas`) tries each option in order and returns the first success. + +```csharp +using ZodSharp; + +var schema = Z.Union(Z.String(), Z.Number(), Z.Boolean()); +var result = schema.Validate(42.0); // matches Z.Number() +``` + +When no option matches, a single `invalid_union` error is produced (`"Value does not match any of the union options"`) whose `Parameters["errors"]` carries every option's errors. + +> [!NOTE] +> On the failure path the accumulated option errors are collected, which allocates. Matching a later option (e.g. number or boolean after a string option) also allocates while the earlier options are attempted — see [Performance](Performance.md). + +## Typed union + +`ZodTypedUnion` dispatches on runtime type (`is T1` / `is T2`) and produces a `Union` result value. + +```csharp +var schema = Z.Union(Z.String(), Z.Number()); +var result = schema.Validate(42.0); + +if (result.IsSuccess) + result.Value.Match( + str => Console.WriteLine($"string: {str}"), + num => Console.WriteLine($"number: {num}")); +``` + +## Union and Union + +The `Union<...>` value type (namespace `ZodSharp.Unions`) is the result of a typed union: + +- `Create(T1)` / `Create(T2)` (and a three-case variant) — tagged construction. +- Implicit conversions from the case types. +- `int Tag` and `object Value` (`Value` throws if uninitialized). +- `TryGetValue(out T1)` / `TryGetValue(out T2)`. +- `Match(Func, Func)` and `Switch(Action, Action)`. +- `==` / `!=`, `Equals`, `GetHashCode`, `ToString`. + +## Discriminated union + +`ZodDiscriminatedUnion` dispatches on a discriminator value read from the input — a dictionary key or a public instance property — resolved case-insensitively. + +```csharp +var union = Z.DiscriminatedUnion("type") + .Option("user", userSchema) + .Option("admin", adminSchema) + .Build(); + +var result = union.Validate(new Dictionary +{ + { "type", "user" }, + { "name", "John" } +}); +``` + +Failures: + +- No discriminator present → `missing_discriminator`. +- Value not among the options → `invalid_discriminator` listing the expected values. +- `null` input → `invalid_type`. + +The builder (`ZodDiscriminatedUnionBuilder`) accepts untyped `IZodSchema` options via `Option(string value, IZodSchema schema)` and typed options via `Option(string value, IZodSchema schema)`, which wrap the schema for coercion and `null` handling. + +## Intersection + +`ZodIntersection` (created with `Z.Intersection(left, right)` or `.And(other)`) succeeds only when both schemas validate; failures merge both error sets. + +```csharp +var schema = Z.String().Min(3).And(Z.String().Max(10)); +``` \ No newline at end of file diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md new file mode 100644 index 0000000..c760469 --- /dev/null +++ b/docs/wiki/_Sidebar.md @@ -0,0 +1,25 @@ +- [Home](Home.md) +- [Getting Started](Getting-Started.md) +- [Core Concepts](Core-Concepts.md) +- [Fluent Schema API](Fluent-Schema-API.md) +- [String Validation](String-Validation.md) +- [Number Validation](Number-Validation.md) +- [Object Validation](Object-Validation.md) +- [Arrays and Other Schemas](Arrays-and-Other-Schemas.md) +- [Unions and Discriminated Unions](Unions-and-Discriminated-Unions.md) +- [Composition and Transforms](Composition-and-Transforms.md) +- [Compiled Validators and Caching](Compiled-Validators-and-Caching.md) +- [JSON Schema Export](JsonSchema-Export.md) +- [JSON Schema Import](JsonSchema-Import.md) +- [System.Text.Json Integration](SystemTextJson-Integration.md) +- [Newtonsoft.Json Integration](NewtonsoftJson-Integration.md) +- [ASP.NET Core Integration](AspNetCore-Integration.md) +- [Dependency Injection](Dependency-Injection.md) +- [Source Generator](Source-Generator.md) +- [Source Generator DataAnnotations](Source-Generator-DataAnnotations.md) +- [Source Generator Diagnostics](Source-Generator-Diagnostics.md) +- [Cross-Platform Interop](Cross-Platform-Interop.md) +- [Performance](Performance.md) +- [Guarantees and Limitations](Guarantees-and-Limitations.md) +- [Release Flow](Release-Flow.md) +- [Contributing](Contributing.md) \ No newline at end of file diff --git a/package.json b/package.json index 1b74415..998724e 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "name": "Kieron Lanning", "url": "https://kieronlanning.dev/" }, - "homepage": "https://github.com/purview-dev/zodsharp#readme", + "homepage": "https://purview.dev/projects/zodsharp/", "bugs": { "url": "https://github.com/purview-dev/zodsharp/issues" }, diff --git a/src/Directory.Build.props b/src/Directory.Build.props index cd59704..30312c5 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -20,13 +20,19 @@ $(ZodSharpNetTargetFrameworks) - + + https://purview.dev/ + $(PurviewHomepage)projects/zodsharp/ + $(PurviewHomepage)docs/zodsharp/ + + + ZodSharp Contributors;Purview-Dev Contributors - https://github.com/purview-dev/zodsharp Purview.dev - $(RepositoryUrl) + $(PurviewProjectUrl) $(RepositoryUrl) git + true purview-logo.png README.md MIT diff --git a/src/src/AspNetCore/Sdk/README.md b/src/src/AspNetCore/Sdk/README.md index 2ee0d1d..8b233f3 100644 --- a/src/src/AspNetCore/Sdk/README.md +++ b/src/src/AspNetCore/Sdk/README.md @@ -38,9 +38,10 @@ A `ValidationProblemDetails` overload is also available: var problem = result.ToValidationProblemDetails(); ``` -## Further reading +## Documentation -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). +- [Homepage](https://purview.dev/projects/zodsharp/) +- [Documentation](https://purview.dev/docs/zodsharp/) ## License diff --git a/src/src/NewtonsoftJson/Sdk/README.md b/src/src/NewtonsoftJson/Sdk/README.md index 0be7b44..4737cec 100644 --- a/src/src/NewtonsoftJson/Sdk/README.md +++ b/src/src/NewtonsoftJson/Sdk/README.md @@ -54,9 +54,10 @@ 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 +## Documentation -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). +- [Homepage](https://purview.dev/projects/zodsharp/) +- [Documentation](https://purview.dev/docs/zodsharp/) ## License diff --git a/src/src/SystemTextJson/Sdk/README.md b/src/src/SystemTextJson/Sdk/README.md index 2652910..7af1726 100644 --- a/src/src/SystemTextJson/Sdk/README.md +++ b/src/src/SystemTextJson/Sdk/README.md @@ -52,9 +52,10 @@ 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 +## Documentation -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). +- [Homepage](https://purview.dev/projects/zodsharp/) +- [Documentation](https://purview.dev/docs/zodsharp/) ## License diff --git a/src/src/ZodSharp/Sdk/README.md b/src/src/ZodSharp/Sdk/README.md index 9a019ea..5e0a39e 100644 --- a/src/src/ZodSharp/Sdk/README.md +++ b/src/src/ZodSharp/Sdk/README.md @@ -76,9 +76,10 @@ DataAnnotations attributes such as `[Required]`, `[Length]`, `[StringLength]`, ` var jsonSchema = Z.ToJsonSchema(userSchema, new ToJsonSchemaOptions { Title = "User" }); ``` -## Further reading +## Documentation -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). +- [Homepage](https://purview.dev/projects/zodsharp/) +- [Documentation](https://purview.dev/docs/zodsharp/) ## License