diff --git a/README.md b/README.md index c63915b..1406299 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ The fixture generator script is intentionally run via Bun rather than `npx tsx` - **Packages are published under the `Purview.*` IDs.** The core `Purview.ZodSharp` package ships the validator and the source generator, with optional `Purview.ZodSharp.SystemTextJson`, `Purview.ZodSharp.NewtonsoftJson`, and `Purview.ZodSharp.AspNetCore` integration packages. - **Targets `net8.0`, `net9.0`, and `net10.0`.** The source generator remains on `netstandard2.0` so it can run in any compiler host. - **System.Text.Json integration.** JSON deserialize-and-validate is available for both major JSON libraries, including validating `JsonConverter` instances. -- **JSON Schema interoperability.** Schemas can be exported via `Z.ToJsonSchema` and imported via `Z.FromJsonSchema`, enabling cross-language reuse with TypeScript/Zod. +- **JSON Schema interoperability.** Schemas can be exported via `Z.ToJsonSchema` and imported via `Z.FromJsonSchema`, enabling cross-language reuse with TypeScript/Zod. The import API lives in the JSON integration package's namespace (`ZodSharp.JsonSchema.SystemTextJson` or `ZodSharp.JsonSchema.NewtonsoftJson`); export stays in the core package. - **ASP.NET Core ProblemDetails integration.** Failed validation results convert directly to `HttpValidationProblemDetails` via `result.ToHttpValidationProblemDetails()`. - **Expanded DataAnnotations support.** `[Length]`, `[MinLength]`, `[MaxLength]`, `[RegularExpression]`, `[AllowedValues]`, `[DeniedValues]`, `[EmailAddress]`, and more, with structured size failures (`Code`, `Origin`, `Minimum`/`Maximum`, `Inclusive`, `Path`). - **Value-first composition methods.** `.ApplyAnd()`, `.ApplyOr()`, and `.ApplyRefine()` are the supported composition surface. @@ -430,19 +430,22 @@ var jsonSchema = Z.ToJsonSchema>(userSchema, new ToJ // Serialize with your preferred JSON library // System.Text.Json (add Purview.ZodSharp.SystemTextJson): -using ZodSharp.JsonSchema; +using ZodSharp.JsonSchema.SystemTextJson; var systemTextJson = System.Text.Json.JsonSerializer.Serialize(jsonSchema, JsonSchemaSerializerOptions.Default); // Newtonsoft.Json (add Purview.ZodSharp.NewtonsoftJson): -using ZodSharp.JsonSchema; +using ZodSharp.JsonSchema.NewtonsoftJson; var newtonsoftJson = JsonConvert.SerializeObject(jsonSchema, JsonSchemaSerializerOptions.Default); ``` #### Import from JSON Schema (JSON Schema -> Purview.ZodSharp) -Add either `Purview.ZodSharp.SystemTextJson` or `Purview.ZodSharp.NewtonsoftJson` to your project, then: +Add either `Purview.ZodSharp.SystemTextJson` or `Purview.ZodSharp.NewtonsoftJson` to your project, then import that package's JSON Schema namespace: ```csharp +using ZodSharp; +using ZodSharp.JsonSchema.SystemTextJson; // or ZodSharp.JsonSchema.NewtonsoftJson + var jsonSchemaString = @"{ ""type"": ""object"", ""properties"": { diff --git a/docs/wiki/AspNetCore-Integration.md b/docs/wiki/AspNetCore-Integration.md index 5e6b0c3..0520f30 100644 --- a/docs/wiki/AspNetCore-Integration.md +++ b/docs/wiki/AspNetCore-Integration.md @@ -196,10 +196,21 @@ The generated `Create` builds a typed `ErrorTypeParameters` instance (validated parameter types and exposed through `ValidationError.Parameters`) and sets the message from `ErrorType.FormatMessage`, so `error.Message` already reads `Aggregate 'agg-123' (of type Invoice) failed to save` and mapping through the registry produces the -`409 Conflict` response described below. The analyzers `ZODSASP001`/`ZODSASP002`/`ZODSASP003` -(shipped with the core package) warn when a `MessageFormat` placeholder is not declared in -`Parameters`, when an `[ErrorType]` field's containing class is not `partial`, or when the field is not -`static readonly`. +`409 Conflict` response described below. + +### ErrorType diagnostics + +The `ErrorType` factory, its source generator, and its analyzers ship with the core `Purview.ZodSharp` package: + +| ID | Severity | Meaning | +|---|---|---| +| `ZODSASP001` | Warning | A `MessageFormat` placeholder is not declared in `ErrorType.Parameters` | +| `ZODSASP002` | Warning | The containing type of an `[ErrorType]` field is not declared `partial` | +| `ZODSASP003` | Warning | An `[ErrorType]` field is not declared `static readonly` | +| `ZODSASP100` | Error | Unhandled exception in the `ErrorType` source generator | +| `ZODSASP101` | Error | The `Parameters` of an `[ErrorType]` field could not be extracted | + +`ZODSASP001`–`ZODSASP003` explain why `Create`/`Throw` helpers were not generated; `ZODSASP100`/`ZODSASP101` are fatal generator failures that name the field they failed on. Produces a `409 Conflict` `HttpValidationProblemDetails` with: diff --git a/docs/wiki/Cross-Platform-Interop.md b/docs/wiki/Cross-Platform-Interop.md index 0639fdc..0ae4eb9 100644 --- a/docs/wiki/Cross-Platform-Interop.md +++ b/docs/wiki/Cross-Platform-Interop.md @@ -41,7 +41,7 @@ If the C# output directory is empty, the vitest suite emits a note instructing y 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). +- Import: `Z.FromJsonSchema` (in the System.Text.Json or Newtonsoft.Json package — namespace `ZodSharp.JsonSchema.SystemTextJson` / `ZodSharp.JsonSchema.NewtonsoftJson`). See [JSON Schema Export](JsonSchema-Export.md) and [JSON Schema Import](JsonSchema-Import.md). diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md index dbd5eef..5a3bbb0 100644 --- a/docs/wiki/Getting-Started.md +++ b/docs/wiki/Getting-Started.md @@ -25,7 +25,7 @@ dotnet add package Purview.ZodSharp.AspNetCore - `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. +> JSON Schema import (`Z.FromJsonSchema`) is provided by whichever JSON integration package you reference, so pick one, and import its JSON Schema namespace (`ZodSharp.JsonSchema.SystemTextJson` or `ZodSharp.JsonSchema.NewtonsoftJson`). Export (`Z.ToJsonSchema`) lives in the core package. ## First schema @@ -116,6 +116,7 @@ var result = userSchema.DeserializeAndValidate(json); var jsonSchema = Z.ToJsonSchema(userSchema, new ToJsonSchemaOptions { Title = "User" }); // JSON Schema import (requires an integration package) +// using ZodSharp.JsonSchema.SystemTextJson; // or ZodSharp.JsonSchema.NewtonsoftJson var imported = Z.FromJsonSchema(jsonSchemaString); ``` diff --git a/docs/wiki/Guarantees-and-Limitations.md b/docs/wiki/Guarantees-and-Limitations.md index f58b8c2..e557e15 100644 --- a/docs/wiki/Guarantees-and-Limitations.md +++ b/docs/wiki/Guarantees-and-Limitations.md @@ -49,11 +49,11 @@ Rules evaluated by the base `Validate` pipeline produce `validation_failed` erro ### JSON Schema import scope -`Z.FromJsonSchema` supports **local** `$ref` (`#/...`) references only; external `$ref` targets throw `NotSupportedException`. `FromJsonSchemaOptions` is currently empty (reserved for future options). +`Z.FromJsonSchema` supports **local** `$ref` (`#/...`) references only; external `$ref` targets throw a `NotSupportedException` that names the unsupported reference. The integration packages' `JsonSchemaSerializerOptions` read and write the JSON Schema keyword names (`$schema`, `$id`, `$ref`, `$defs`), so exported definitions round-trip; the core `JsonSchemaDefinition` type itself stays free of serializer annotations. Import types live in package-specific namespaces (`ZodSharp.JsonSchema.SystemTextJson` / `ZodSharp.JsonSchema.NewtonsoftJson`). ### 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`. +`Purview.ZodSharp.SystemTextJson` and `Purview.ZodSharp.NewtonsoftJson` are **mutually exclusive** integrations — pick the one that matches your JSON library. Both packages can be referenced from the same project **without `extern alias`**, because every JSON Schema import type is declared in a package-specific namespace (`ZodSharp.JsonSchema.SystemTextJson`, `ZodSharp.JsonSchema.NewtonsoftJson`) rather than an identical full name. The deserialize/serialize extension methods remain in the `ZodSharp` namespace in both packages, so import exactly one package namespace per file: importing both makes calls such as `schema.DeserializeAndValidate(json)` or `Z.FromJsonSchema(...)` ambiguous at the call site. ## Custom rules diff --git a/docs/wiki/JsonSchema-Export.md b/docs/wiki/JsonSchema-Export.md index b27ea22..4c62ee8 100644 --- a/docs/wiki/JsonSchema-Export.md +++ b/docs/wiki/JsonSchema-Export.md @@ -55,15 +55,15 @@ Pick the JSON serializer that matches the integration package you referenced: ```csharp // System.Text.Json (Purview.ZodSharp.SystemTextJson) -using ZodSharp.JsonSchema; +using ZodSharp.JsonSchema.SystemTextJson; var json = System.Text.Json.JsonSerializer.Serialize(jsonSchema, JsonSchemaSerializerOptions.Default); // Newtonsoft.Json (Purview.ZodSharp.NewtonsoftJson) -using ZodSharp.JsonSchema; +using ZodSharp.JsonSchema.NewtonsoftJson; var json = JsonConvert.SerializeObject(jsonSchema, JsonSchemaSerializerOptions.Default); ``` -`JsonSchemaSerializerOptions.Default` (camelCase, ignore nulls, indented) and `.Reading` (camelCase, ignore nulls) are provided by each integration package. +`JsonSchemaSerializerOptions.Default` (camelCase, ignore nulls, indented) and `.Reading` (camelCase, ignore nulls) are provided by each integration package and map the JSON Schema keyword names (`$schema`, `$id`, `$ref`, `$defs`), with camelCase for every other keyword. `JsonSchemaDefinition` itself carries no serializer annotations, so use these options when serializing it — that also keeps exported definitions round-tripping through the matching import API. ## Round-trip diff --git a/docs/wiki/JsonSchema-Import.md b/docs/wiki/JsonSchema-Import.md index 092fc9b..3bd59a6 100644 --- a/docs/wiki/JsonSchema-Import.md +++ b/docs/wiki/JsonSchema-Import.md @@ -1,9 +1,10 @@ # JSON Schema Import -Import a JSON Schema into a Purview.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). +Import a JSON Schema into a Purview.ZodSharp schema with `Z.FromJsonSchema`. This API is provided by the JSON integration packages — reference either `Purview.ZodSharp.SystemTextJson` or `Purview.ZodSharp.NewtonsoftJson`. The two JSON integrations are mutually exclusive; pick one and import its JSON Schema namespace (`ZodSharp.JsonSchema.SystemTextJson` or `ZodSharp.JsonSchema.NewtonsoftJson`). ```csharp using ZodSharp; +using ZodSharp.JsonSchema.SystemTextJson; // or ZodSharp.JsonSchema.NewtonsoftJson var jsonSchemaString = """ { @@ -24,17 +25,15 @@ var result = userSchema.Validate(userData); | 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. +| `IZodSchema FromJsonSchema(string jsonSchema)` | parses the JSON string into a `JsonSchemaDefinition`, then into a schema | +| `IZodSchema FromJsonSchema(JsonSchemaDefinition schema)` | import from an already-deserialized definition | > [!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: +`FromJsonSchemaParser` (namespace `ZodSharp.JsonSchema.SystemTextJson` or `ZodSharp.JsonSchema.NewtonsoftJson`) maps: - `type` — `string` / `number` / `integer` / `boolean` / `null` / `object` / `array`. - `enum` → `ZodUnion` of literals (a single member becomes a literal); `const` → literal. @@ -46,8 +45,14 @@ var result = userSchema.Validate(userData); ## 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. +- `$ref` is supported only for **local** references (`#/...`, for example `#/$defs/Name`). External `$ref` targets throw `NotSupportedException` naming the unsupported reference: + + ```text + External $ref 'external.json#/$defs/name' is not supported. Only local references ('#/...', for example '#/$defs/Name') can be resolved; inline or pre-resolve external schemas before importing. + ``` + + Inline the referenced schema, or move it under the root `$defs`, before importing. Local references may be cyclic — a reference that is still being resolved becomes a lazy schema. +- The reader binds the JSON Schema keyword names `$schema`, `$id`, `$ref`, and `$defs` (plus the draft-07 `definitions`) through the integration package's `JsonSchemaSerializerOptions`, and the same options write them back, so exported definitions round-trip through either package. `JsonSchemaDefinition` itself carries no serializer annotations, so serialize it with `JsonSchemaSerializerOptions` to get keyword-compliant output. ## Cross-platform reuse diff --git a/docs/wiki/NewtonsoftJson-Integration.md b/docs/wiki/NewtonsoftJson-Integration.md index b170ccb..7541702 100644 --- a/docs/wiki/NewtonsoftJson-Integration.md +++ b/docs/wiki/NewtonsoftJson-Integration.md @@ -1,6 +1,6 @@ # 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. +The `Purview.ZodSharp.NewtonsoftJson` package adds Newtonsoft.Json deserialize-and-validate, validating converters, and JSON Schema import to the core library. The deserialize/serialize extension methods live in the `ZodSharp` namespace; the JSON Schema import types (and `Z.FromJsonSchema`) live in the `ZodSharp.JsonSchema.NewtonsoftJson` namespace. ## Install @@ -68,7 +68,14 @@ Deserialize/validation failures produce `ValidationError` entries with codes `de ## JSON Schema import -`Z.FromJsonSchema` is available with this package referenced; see [JSON Schema Import](JsonSchema-Import.md). +```csharp +using ZodSharp; +using ZodSharp.JsonSchema.NewtonsoftJson; + +var schema = Z.FromJsonSchema(jsonSchemaString); +``` + +See [JSON Schema Import](JsonSchema-Import.md) for the supported keywords, `$ref` handling, and the `JsonSchemaSerializerOptions` defaults. ## System.Text.Json vs Newtonsoft.Json @@ -82,5 +89,5 @@ Deserialize/validation failures produce `ValidationError` entries with codes `de | 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 +> [!NOTE] +> `Purview.ZodSharp.SystemTextJson` and `Purview.ZodSharp.NewtonsoftJson` are mutually exclusive integrations — pick the one that matches your JSON library. Both packages can be referenced from the same project without `extern alias` (their JSON Schema types live in the `ZodSharp.JsonSchema.SystemTextJson` / `ZodSharp.JsonSchema.NewtonsoftJson` namespaces), but the deserialize/serialize extension overloads share names, so import exactly one package namespace per file. \ No newline at end of file diff --git a/docs/wiki/Release-Flow.md b/docs/wiki/Release-Flow.md index ad9e4e7..b9bcf6b 100644 --- a/docs/wiki/Release-Flow.md +++ b/docs/wiki/Release-Flow.md @@ -4,7 +4,7 @@ Purview.ZodSharp releases are driven by the shared [purview-dev/build](https://g ## 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. +The package version comes from `package.json` (`version` field). The current stable line is `2.0.0`; bump `package.json` to release a new version (prerelease builds use a `MAJOR.MINOR.PATCH-prerelease.N` suffix). 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. diff --git a/docs/wiki/Source-Generator-Diagnostics.md b/docs/wiki/Source-Generator-Diagnostics.md index de5c06b..0cbbac4 100644 --- a/docs/wiki/Source-Generator-Diagnostics.md +++ b/docs/wiki/Source-Generator-Diagnostics.md @@ -1,6 +1,6 @@ # 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. +The `[ZodSchema]` generator ships an analyzer (category `ZodSharp.SourceGenerator`) that reports configuration and usage problems at compile time. Every diagnostic below is enabled by default; `ZODSGEN033` is a warning and the rest are errors. | ID | Meaning | |---|---| @@ -35,6 +35,8 @@ The `[ZodSchema]` generator ships an analyzer (category `ZodSharp.SourceGenerato | ZODSGEN035 | The `OnZodValidate` refinement hook is not declared as `partial void OnZodValidate(RefineCtx context)` (wrong modifiers, return type, or parameters) | | ZODSGEN036 | A member still uses the retired synchronous refinement contract (`IEnumerable Validate()`); implement `OnZodValidate` instead | +IDs `ZODSGEN002` and `ZODSGEN022`–`ZODSGEN026` are intentionally unused; rule identifiers are never renumbered or re-used. + ## 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/SystemTextJson-Integration.md b/docs/wiki/SystemTextJson-Integration.md index 7ecbb35..05594fd 100644 --- a/docs/wiki/SystemTextJson-Integration.md +++ b/docs/wiki/SystemTextJson-Integration.md @@ -1,6 +1,6 @@ # 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. +The `Purview.ZodSharp.SystemTextJson` package adds System.Text.Json deserialize-and-validate, validating converters, and JSON Schema import to the core library. The deserialize/serialize extension methods live in the `ZodSharp` namespace; the JSON Schema import types (and `Z.FromJsonSchema`) live in the `ZodSharp.JsonSchema.SystemTextJson` namespace. ## Install @@ -65,7 +65,14 @@ Deserialize/validation failures produce `ValidationError` entries with codes `de ## JSON Schema import -`Z.FromJsonSchema` is available with this package referenced; see [JSON Schema Import](JsonSchema-Import.md). +```csharp +using ZodSharp; +using ZodSharp.JsonSchema.SystemTextJson; + +var schema = Z.FromJsonSchema(jsonSchemaString); +``` + +See [JSON Schema Import](JsonSchema-Import.md) for the supported keywords, `$ref` handling, and the `JsonSchemaSerializerOptions` defaults. ## Comparing with Newtonsoft diff --git a/package.json b/package.json index 29fd92c..3f9b711 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zodsharp", - "version": "2.0.0-prerelease.28", + "version": "2.0.0", "private": true, "license": "MIT", "author": { diff --git a/src/ZodSharp.slnx b/src/ZodSharp.slnx index 92074a0..f6e8183 100644 --- a/src/ZodSharp.slnx +++ b/src/ZodSharp.slnx @@ -14,6 +14,7 @@ + diff --git a/src/src/Examples.CLI/JsonSchemaExamples.cs b/src/src/Examples.CLI/JsonSchemaExamples.cs index 71e65ee..3a56d8e 100644 --- a/src/src/Examples.CLI/JsonSchemaExamples.cs +++ b/src/src/Examples.CLI/JsonSchemaExamples.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using ZodSharp.JsonSchema; +using ZodSharp.JsonSchema.NewtonsoftJson; namespace ZodSharp.Examples.CLI; diff --git a/src/src/NewtonsoftJson/JsonSchema/JsonSchemaSerializerOptions.cs b/src/src/NewtonsoftJson/JsonSchema/JsonSchemaSerializerOptions.cs deleted file mode 100644 index 0836906..0000000 --- a/src/src/NewtonsoftJson/JsonSchema/JsonSchemaSerializerOptions.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Newtonsoft.Json; - -namespace ZodSharp.JsonSchema; - -/// -/// Options for JSON serialization/deserialization of JSON Schema. -/// -public static class JsonSchemaSerializerOptions -{ - /// - /// Default settings for JSON Schema serialization. - /// Uses camelCase property naming and ignores null values. - /// - public static readonly JsonSerializerSettings Default = new() - { - NullValueHandling = NullValueHandling.Ignore, - Formatting = Formatting.Indented, - ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver(), - }; - - /// - /// Settings for reading JSON Schema with flexible property matching. - /// - public static readonly JsonSerializerSettings Reading = new() - { - NullValueHandling = NullValueHandling.Ignore, - MissingMemberHandling = MissingMemberHandling.Ignore, - }; -} diff --git a/src/src/NewtonsoftJson/JsonSchema/FromJsonSchemaParser.cs b/src/src/NewtonsoftJson/JsonSchema/NewtonsoftJson/FromJsonSchemaParser.cs similarity index 96% rename from src/src/NewtonsoftJson/JsonSchema/FromJsonSchemaParser.cs rename to src/src/NewtonsoftJson/JsonSchema/NewtonsoftJson/FromJsonSchemaParser.cs index ec6a3c2..f4dfeaa 100644 --- a/src/src/NewtonsoftJson/JsonSchema/FromJsonSchemaParser.cs +++ b/src/src/NewtonsoftJson/JsonSchema/NewtonsoftJson/FromJsonSchemaParser.cs @@ -5,15 +5,7 @@ using ZodSharp.Core; using ZodSharp.Schemas; -namespace ZodSharp.JsonSchema; - -/// -/// Options for parsing JSON Schema to ZodSharp schemas. -/// -public class FromJsonSchemaOptions -{ - // Reserved for future options -} +namespace ZodSharp.JsonSchema.NewtonsoftJson; /// /// Parses JSON Schema (Draft 2020-12) to ZodSharp schemas. @@ -25,13 +17,8 @@ public static class FromJsonSchemaParser /// Parses a JSON Schema definition to a ZodSharp schema. /// /// The JSON Schema definition - /// Parsing options /// A ZodSharp schema that validates according to the JSON Schema - public static IZodSchema Parse(JsonSchemaDefinition schema, -#pragma warning disable IDE0060 // Remove unused parameter - FromJsonSchemaOptions? options = null -#pragma warning restore IDE0060 // Remove unused parameter - ) + public static IZodSchema Parse(JsonSchemaDefinition schema) { if (schema is null) { @@ -47,9 +34,8 @@ public static IZodSchema Parse(JsonSchemaDefinition schema, /// Parses a JSON Schema string to a ZodSharp schema. /// /// The JSON Schema as a string - /// Parsing options /// A ZodSharp schema that validates according to the JSON Schema - public static IZodSchema Parse(string jsonSchema, FromJsonSchemaOptions? options = null) + public static IZodSchema Parse(string jsonSchema) { var schema = JsonConvert.DeserializeObject( jsonSchema, @@ -58,7 +44,7 @@ public static IZodSchema Parse(string jsonSchema, FromJsonSchema return schema == null ? throw new ArgumentException("Invalid JSON Schema: could not parse JSON", nameof(jsonSchema)) - : Parse(schema, options); + : Parse(schema); } sealed class ConversionContext(JsonSchemaDefinition rootSchema, Dictionary defs) @@ -74,6 +60,14 @@ sealed class ConversionContext(JsonSchemaDefinition rootSchema, Dictionary ConvertSchema(JsonSchemaDefinition schema, ConversionContext ctx) { + if (schema is null) + { + throw new ArgumentException( + "The JSON Schema definition contains a null sub-schema, which cannot be converted", + nameof(schema) + ); + } + // Handle $ref if (schema.Ref != null) { @@ -258,7 +252,9 @@ static IZodSchema ResolveRef(string refPath, ConversionContext c { if (!refPath.StartsWith('#')) { - throw new NotSupportedException("External $ref is not supported, only local refs (#/...) are allowed"); + throw new NotSupportedException( + $"External $ref '{refPath}' is not supported. Only local references ('#/...', for example '#/$defs/Name') can be resolved; inline or pre-resolve external schemas before importing." + ); } // Check if already resolved diff --git a/src/src/NewtonsoftJson/JsonSchema/NewtonsoftJson/JsonSchemaSerializerOptions.cs b/src/src/NewtonsoftJson/JsonSchema/NewtonsoftJson/JsonSchemaSerializerOptions.cs new file mode 100644 index 0000000..261dcce --- /dev/null +++ b/src/src/NewtonsoftJson/JsonSchema/NewtonsoftJson/JsonSchemaSerializerOptions.cs @@ -0,0 +1,54 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace ZodSharp.JsonSchema.NewtonsoftJson; + +/// +/// Names the JSON Schema keyword properties with their specification names ($schema, $id, +/// $ref and $defs) and camelCase for every other member. +/// +sealed class JsonSchemaNamingStrategy : CamelCaseNamingStrategy +{ + static readonly Dictionary KeywordNames = new(StringComparer.Ordinal) + { + ["Schema"] = "$schema", + ["Id"] = "$id", + ["Ref"] = "$ref", + ["Defs"] = "$defs", + }; + + public override string GetPropertyName(string name, bool hasSpecifiedName) => + KeywordNames.TryGetValue(name, out var keywordName) + ? keywordName + : base.GetPropertyName(name, hasSpecifiedName); +} + +/// +/// Options for JSON serialization/deserialization of JSON Schema. +/// +public static class JsonSchemaSerializerOptions +{ + /// + /// Default settings for JSON Schema serialization. + /// Writes the JSON Schema keyword names ($schema, $id, $ref, $defs) and + /// camelCase for every other member, and ignores null values. + /// + public static readonly JsonSerializerSettings Default = new() + { + NullValueHandling = NullValueHandling.Ignore, + Formatting = Formatting.Indented, + MetadataPropertyHandling = MetadataPropertyHandling.Ignore, + ContractResolver = new DefaultContractResolver { NamingStrategy = new JsonSchemaNamingStrategy() }, + }; + + /// + /// Settings for reading JSON Schema with flexible property matching. + /// + public static readonly JsonSerializerSettings Reading = new() + { + NullValueHandling = NullValueHandling.Ignore, + MissingMemberHandling = MissingMemberHandling.Ignore, + MetadataPropertyHandling = MetadataPropertyHandling.Ignore, + ContractResolver = new DefaultContractResolver { NamingStrategy = new JsonSchemaNamingStrategy() }, + }; +} diff --git a/src/src/NewtonsoftJson/ZExtensions.cs b/src/src/NewtonsoftJson/JsonSchema/NewtonsoftJson/ZExtensions.cs similarity index 61% rename from src/src/NewtonsoftJson/ZExtensions.cs rename to src/src/NewtonsoftJson/JsonSchema/NewtonsoftJson/ZExtensions.cs index 8c16935..d609cd8 100644 --- a/src/src/NewtonsoftJson/ZExtensions.cs +++ b/src/src/NewtonsoftJson/JsonSchema/NewtonsoftJson/ZExtensions.cs @@ -1,7 +1,6 @@ using ZodSharp.Core; -using ZodSharp.JsonSchema; -namespace ZodSharp; +namespace ZodSharp.JsonSchema.NewtonsoftJson; [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible")] #if !NETSTANDARD2_1_OR_GREATER @@ -15,18 +14,14 @@ public static class ZExtensions /// Creates a ZodSharp schema from a JSON Schema definition. /// Enables consuming schemas defined in TypeScript Zod. /// - public static IZodSchema FromJsonSchema( - JsonSchemaDefinition schema, - FromJsonSchemaOptions? options = null - ) => FromJsonSchemaParser.Parse(schema, options); + public static IZodSchema FromJsonSchema(JsonSchemaDefinition schema) => + FromJsonSchemaParser.Parse(schema); /// /// Creates a ZodSharp schema from a JSON Schema string. /// Enables consuming schemas defined in TypeScript Zod via JSON files or APIs. /// - public static IZodSchema FromJsonSchema( - string jsonSchema, - FromJsonSchemaOptions? options = null - ) => FromJsonSchemaParser.Parse(jsonSchema, options); + public static IZodSchema FromJsonSchema(string jsonSchema) => + FromJsonSchemaParser.Parse(jsonSchema); } } diff --git a/src/src/NewtonsoftJson/Sdk/README.md b/src/src/NewtonsoftJson/Sdk/README.md index 4737cec..71d6909 100644 --- a/src/src/NewtonsoftJson/Sdk/README.md +++ b/src/src/NewtonsoftJson/Sdk/README.md @@ -48,6 +48,9 @@ var value = JsonConvert.DeserializeObject(json, converter); ## Import from JSON Schema ```csharp +using ZodSharp; +using ZodSharp.JsonSchema.NewtonsoftJson; + var schema = Z.FromJsonSchema(jsonSchemaString); var result = schema.Validate(data); ``` diff --git a/src/src/SourceGenerators/AnalyzerReleases.Shipped.md b/src/src/SourceGenerators/AnalyzerReleases.Shipped.md index dae3318..0c5d06d 100644 --- a/src/src/SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/src/SourceGenerators/AnalyzerReleases.Shipped.md @@ -6,7 +6,7 @@ |---|---|---|---| | ZODSGEN001 | ZodSharp.SourceGenerator | Error | This is effectively a fatal incident where the source generator has failed. | -## Release 2.0 +## Release 2.0.0 ### New Rules @@ -16,16 +16,33 @@ | ZODSGEN004 | ZodSharp.SourceGenerator | Error | Unsupported LengthAttribute target | ZODSGEN005 | ZodSharp.SourceGenerator | Error | Invalid DataAnnotations error message resource configuration | ZODSGEN006 | ZodSharp.SourceGenerator | Error | Unsupported DataAnnotations usage -| ZODSGEN007 | ZodSharp.SourceGenerator | Error | Custom validation method not found | -| ZODSGEN008 | ZodSharp.SourceGenerator | Error | Invalid return type for custom validation method | -| ZODSGEN009 | ZodSharp.SourceGenerator | Error | Invalid custom validation method parameter count | -| ZODSGEN010 | ZodSharp.SourceGenerator | Error | Invalid custom validation model parameter | -| ZODSGEN011 | ZodSharp.SourceGenerator | Error | Missing or invalid CancellationToken parameter | -| ZODSGEN012 | ZodSharp.SourceGenerator | Error | Generic custom validation method unsupported | -| ZODSGEN013 | ZodSharp.SourceGenerator | Error | Custom validation method must be static when defined on model type | -| ZODSGEN014 | ZodSharp.SourceGenerator | Error | Inaccessible custom validation method | -| ZODSGEN015 | ZodSharp.SourceGenerator | Error | Ambiguous custom validation method overloads | -| ZODSGEN016 | ZodSharp.SourceGenerator | Error | Invalid custom validation method name | -| ZODSGEN017 | ZodSharp.SourceGenerator | Error | Abstract custom validation method unsupported | -| ZODSGEN018 | ZodSharp.SourceGenerator | Error | Partial custom validation method has not been implemented | -| ZODSGEN019 | ZodSharp.SourceGenerator | Error | Invalid custom validation method parameter modifier | +| ZODSGEN007 | ZodSharp.SourceGenerator | Error | Custom validation method not found +| ZODSGEN008 | ZodSharp.SourceGenerator | Error | Invalid return type for custom validation method +| ZODSGEN009 | ZodSharp.SourceGenerator | Error | Invalid custom validation method parameter count +| ZODSGEN010 | ZodSharp.SourceGenerator | Error | Invalid custom validation model parameter +| ZODSGEN011 | ZodSharp.SourceGenerator | Error | Missing or invalid CancellationToken parameter +| ZODSGEN012 | ZodSharp.SourceGenerator | Error | Generic custom validation method unsupported +| ZODSGEN013 | ZodSharp.SourceGenerator | Error | Custom validation method must be static when defined on model type +| ZODSGEN014 | ZodSharp.SourceGenerator | Error | Inaccessible custom validation method +| ZODSGEN015 | ZodSharp.SourceGenerator | Error | Ambiguous custom validation method overloads +| ZODSGEN016 | ZodSharp.SourceGenerator | Error | Invalid custom validation method name +| ZODSGEN017 | ZodSharp.SourceGenerator | Error | Abstract custom validation method unsupported +| ZODSGEN018 | ZodSharp.SourceGenerator | Error | Partial custom validation method has not been implemented +| ZODSGEN019 | ZodSharp.SourceGenerator | Error | Invalid custom validation method parameter modifier +| ZODSGEN020 | ZodSharp.SourceGenerator | Error | CompareAttribute references an unknown property +| ZODSGEN021 | ZodSharp.SourceGenerator | Error | Add a reference to System.ComponentModel.DataAnnotations +| ZODSGEN027 | ZodSharp.SourceGenerator | Error | IValidateOptions generation requires a reference to Microsoft.Extensions.Options +| ZODSGEN028 | ZodSharp.SourceGenerator | Error | IValidateOptions generation requires a reference type +| ZODSGEN029 | ZodSharp.SourceGenerator | Error | OnZodValidate refinement hook and async custom validation methods are mutually exclusive +| ZODSGEN030 | ZodSharp.SourceGenerator | Error | Custom rule does not implement IValidationRule for the property type (or an unbound generic rule cannot be closed with it) +| ZODSGEN031 | ZodSharp.SourceGenerator | Error | Unable to map an attribute value to a custom rule constructor parameter +| ZODSGEN032 | ZodSharp.SourceGenerator | Error | Unable to generate a validation attribute for a custom rule +| ZODSGEN033 | ZodSharp.SourceGenerator | Warning | Rule attribute is applied to a type that gets no generated schema +| ZODSGEN034 | ZodSharp.SourceGenerator | Error | OnZodValidate refinement hook requires the target type (and its containing types) to be declared partial +| ZODSGEN035 | ZodSharp.SourceGenerator | Error | OnZodValidate refinement hook must be declared as 'partial void OnZodValidate(RefineCtx context)' +| ZODSGEN036 | ZodSharp.SourceGenerator | Error | The synchronous refinement method contract has been replaced by the OnZodValidate hook +| ZODSASP001 | ZodSharp.SourceGenerator | Warning | MessageFormat placeholder is not declared in Parameters +| ZODSASP002 | ZodSharp.SourceGenerator | Warning | Error type containing type must be partial +| ZODSASP003 | ZodSharp.SourceGenerator | Warning | ErrorType field must be static readonly +| ZODSASP100 | ZodSharp.SourceGenerator | Error | Unhandled exception in the ErrorType source generator +| ZODSASP101 | ZodSharp.SourceGenerator | Error | ErrorType Parameters could not be extracted diff --git a/src/src/SourceGenerators/AnalyzerReleases.Unshipped.md b/src/src/SourceGenerators/AnalyzerReleases.Unshipped.md index 57a70fd..7b845f4 100644 --- a/src/src/SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/src/src/SourceGenerators/AnalyzerReleases.Unshipped.md @@ -1,21 +1,7 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + ### New Rules Rule ID | Category | Severity | Notes ---------|----------|----------|------ -ZODSGEN020 | ZodSharp.SourceGenerator | Error | CompareAttribute references an unknown property -ZODSGEN021 | ZodSharp.SourceGenerator | Error | Add a reference to System.ComponentModel.DataAnnotations -ZODSGEN027 | ZodSharp.SourceGenerator | Error | IValidateOptions generation requires a reference to Microsoft.Extensions.Options -ZODSGEN028 | ZodSharp.SourceGenerator | Error | IValidateOptions generation requires a reference type -ZODSGEN029 | ZodSharp.SourceGenerator | Error | OnZodValidate refinement hook and async custom validation methods are mutually exclusive -ZODSGEN030 | ZodSharp.SourceGenerator | Error | Custom rule does not implement IValidationRule for the property type (or an unbound generic rule cannot be closed with it) -ZODSGEN031 | ZodSharp.SourceGenerator | Error | Unable to map an attribute value to a custom rule constructor parameter -ZODSGEN032 | ZodSharp.SourceGenerator | Error | Unable to generate a validation attribute for a custom rule -ZODSGEN033 | ZodSharp.SourceGenerator | Warning | Rule attribute is applied to a type that gets no generated schema -ZODSGEN034 | ZodSharp.SourceGenerator | Error | OnZodValidate refinement hook requires the target type (and its containing types) to be declared partial -ZODSGEN035 | ZodSharp.SourceGenerator | Error | OnZodValidate refinement hook must be declared as 'partial void OnZodValidate(RefineCtx context)' -ZODSGEN036 | ZodSharp.SourceGenerator | Error | The synchronous refinement method contract has been replaced by the OnZodValidate hook -ZODSASP001 | ZodSharp.SourceGenerator | Warning | MessageFormat placeholder is not declared in Parameters -ZODSASP002 | ZodSharp.SourceGenerator | Warning | Error type containing type must be partial -ZODSASP003 | ZodSharp.SourceGenerator | Warning | ErrorType field must be static readonly -ZODSASP100 | ZodSharp.SourceGenerator | Error | Unhandled exception in the ErrorType source generator -ZODSASP101 | ZodSharp.SourceGenerator | Error | ErrorType Parameters could not be extracted +--------|----------|----------|------- diff --git a/src/src/SystemTextJson/JsonSchema/JsonSchemaSerializerOptions.cs b/src/src/SystemTextJson/JsonSchema/JsonSchemaSerializerOptions.cs deleted file mode 100644 index 9edc4b9..0000000 --- a/src/src/SystemTextJson/JsonSchema/JsonSchemaSerializerOptions.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Text.Json; - -namespace ZodSharp.JsonSchema; - -/// -/// Options for JSON serialization/deserialization of JSON Schema using System.Text.Json. -/// -public static class JsonSchemaSerializerOptions -{ - /// - /// Default settings for JSON Schema serialization. - /// Uses camelCase property naming and ignores null values. - /// - public static readonly JsonSerializerOptions Default = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, - WriteIndented = true, - }; - - /// - /// Settings for reading JSON Schema with flexible property matching. - /// - public static readonly JsonSerializerOptions Reading = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, - }; -} diff --git a/src/src/SystemTextJson/JsonSchema/FromJsonSchemaParser.cs b/src/src/SystemTextJson/JsonSchema/SystemTextJson/FromJsonSchemaParser.cs similarity index 96% rename from src/src/SystemTextJson/JsonSchema/FromJsonSchemaParser.cs rename to src/src/SystemTextJson/JsonSchema/SystemTextJson/FromJsonSchemaParser.cs index 348f22a..7426147 100644 --- a/src/src/SystemTextJson/JsonSchema/FromJsonSchemaParser.cs +++ b/src/src/SystemTextJson/JsonSchema/SystemTextJson/FromJsonSchemaParser.cs @@ -4,15 +4,7 @@ using ZodSharp.Core; using ZodSharp.Schemas; -namespace ZodSharp.JsonSchema; - -/// -/// Options for parsing JSON Schema to ZodSharp schemas. -/// -public class FromJsonSchemaOptions -{ - // Reserved for future options -} +namespace ZodSharp.JsonSchema.SystemTextJson; /// /// Parses JSON Schema (Draft 2020-12) to ZodSharp schemas using System.Text.Json. @@ -24,13 +16,8 @@ public static class FromJsonSchemaParser /// Parses a JSON Schema definition to a ZodSharp schema. /// /// The JSON Schema definition - /// Parsing options /// A ZodSharp schema that validates according to the JSON Schema - public static IZodSchema Parse(JsonSchemaDefinition schema, -#pragma warning disable IDE0060 // Remove unused parameter - FromJsonSchemaOptions? options = null -#pragma warning restore IDE0060 // Remove unused parameter - ) + public static IZodSchema Parse(JsonSchemaDefinition schema) { if (schema is null) { @@ -46,15 +33,14 @@ public static IZodSchema Parse(JsonSchemaDefinition schema, /// Parses a JSON Schema string to a ZodSharp schema. /// /// The JSON Schema as a string - /// Parsing options /// A ZodSharp schema that validates according to the JSON Schema - public static IZodSchema Parse(string jsonSchema, FromJsonSchemaOptions? options = null) + public static IZodSchema Parse(string jsonSchema) { var schema = JsonSerializer.Deserialize(jsonSchema, JsonSchemaSerializerOptions.Reading); return schema == null ? throw new ArgumentException("Invalid JSON Schema: could not parse JSON", nameof(jsonSchema)) - : Parse(schema, options); + : Parse(schema); } sealed class ConversionContext(JsonSchemaDefinition rootSchema, Dictionary defs) @@ -70,6 +56,14 @@ sealed class ConversionContext(JsonSchemaDefinition rootSchema, Dictionary ConvertSchema(JsonSchemaDefinition schema, ConversionContext ctx) { + if (schema is null) + { + throw new ArgumentException( + "The JSON Schema definition contains a null sub-schema, which cannot be converted", + nameof(schema) + ); + } + // Handle $ref if (schema.Ref != null) { @@ -254,7 +248,9 @@ static IZodSchema ResolveRef(string refPath, ConversionContext c { if (!refPath.StartsWith('#')) { - throw new NotSupportedException("External $ref is not supported, only local refs (#/...) are allowed"); + throw new NotSupportedException( + $"External $ref '{refPath}' is not supported. Only local references ('#/...', for example '#/$defs/Name') can be resolved; inline or pre-resolve external schemas before importing." + ); } // Check if already resolved diff --git a/src/src/SystemTextJson/JsonSchema/SystemTextJson/JsonSchemaSerializerOptions.cs b/src/src/SystemTextJson/JsonSchema/SystemTextJson/JsonSchemaSerializerOptions.cs new file mode 100644 index 0000000..c8443e5 --- /dev/null +++ b/src/src/SystemTextJson/JsonSchema/SystemTextJson/JsonSchemaSerializerOptions.cs @@ -0,0 +1,50 @@ +using System.Text.Json; + +namespace ZodSharp.JsonSchema.SystemTextJson; + +/// +/// Names the JSON Schema keyword properties with their specification names ($schema, $id, +/// $ref and $defs) and camelCase for every other member. +/// +sealed class JsonSchemaNamingPolicy : JsonNamingPolicy +{ + static readonly Dictionary KeywordNames = new(StringComparer.Ordinal) + { + ["Schema"] = "$schema", + ["Id"] = "$id", + ["Ref"] = "$ref", + ["Defs"] = "$defs", + }; + + public override string ConvertName(string name) => + KeywordNames.TryGetValue(name, out var keywordName) + ? keywordName + : JsonNamingPolicy.CamelCase.ConvertName(name); +} + +/// +/// Options for JSON serialization/deserialization of JSON Schema using System.Text.Json. +/// +public static class JsonSchemaSerializerOptions +{ + /// + /// Default settings for JSON Schema serialization. + /// Writes the JSON Schema keyword names ($schema, $id, $ref, $defs) and + /// camelCase for every other member, and ignores null values. + /// + public static readonly JsonSerializerOptions Default = new() + { + PropertyNamingPolicy = new JsonSchemaNamingPolicy(), + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true, + }; + + /// + /// Settings for reading JSON Schema with flexible property matching. + /// + public static readonly JsonSerializerOptions Reading = new() + { + PropertyNamingPolicy = new JsonSchemaNamingPolicy(), + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, + }; +} diff --git a/src/src/SystemTextJson/ZExtensions.cs b/src/src/SystemTextJson/JsonSchema/SystemTextJson/ZExtensions.cs similarity index 61% rename from src/src/SystemTextJson/ZExtensions.cs rename to src/src/SystemTextJson/JsonSchema/SystemTextJson/ZExtensions.cs index 8c16935..8185c24 100644 --- a/src/src/SystemTextJson/ZExtensions.cs +++ b/src/src/SystemTextJson/JsonSchema/SystemTextJson/ZExtensions.cs @@ -1,7 +1,6 @@ using ZodSharp.Core; -using ZodSharp.JsonSchema; -namespace ZodSharp; +namespace ZodSharp.JsonSchema.SystemTextJson; [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible")] #if !NETSTANDARD2_1_OR_GREATER @@ -15,18 +14,14 @@ public static class ZExtensions /// Creates a ZodSharp schema from a JSON Schema definition. /// Enables consuming schemas defined in TypeScript Zod. /// - public static IZodSchema FromJsonSchema( - JsonSchemaDefinition schema, - FromJsonSchemaOptions? options = null - ) => FromJsonSchemaParser.Parse(schema, options); + public static IZodSchema FromJsonSchema(JsonSchemaDefinition schema) => + FromJsonSchemaParser.Parse(schema); /// /// Creates a ZodSharp schema from a JSON Schema string. /// Enables consuming schemas defined in TypeScript Zod via JSON files or APIs. /// - public static IZodSchema FromJsonSchema( - string jsonSchema, - FromJsonSchemaOptions? options = null - ) => FromJsonSchemaParser.Parse(jsonSchema, options); + public static IZodSchema FromJsonSchema(string jsonSchema) => + FromJsonSchemaParser.Parse(jsonSchema); } } diff --git a/src/src/SystemTextJson/Sdk/README.md b/src/src/SystemTextJson/Sdk/README.md index 7af1726..37528d1 100644 --- a/src/src/SystemTextJson/Sdk/README.md +++ b/src/src/SystemTextJson/Sdk/README.md @@ -46,6 +46,9 @@ var value = JsonSerializer.Deserialize(json, options); ## Import from JSON Schema ```csharp +using ZodSharp; +using ZodSharp.JsonSchema.SystemTextJson; + var schema = Z.FromJsonSchema(jsonSchemaString); var result = schema.Validate(data); ``` diff --git a/src/tests/JsonInterop.UnitTests/Json/NewtonsoftJsonImportTests.cs b/src/tests/JsonInterop.UnitTests/Json/NewtonsoftJsonImportTests.cs new file mode 100644 index 0000000..b2f5326 --- /dev/null +++ b/src/tests/JsonInterop.UnitTests/Json/NewtonsoftJsonImportTests.cs @@ -0,0 +1,68 @@ +using ZodSharp.JsonSchema.NewtonsoftJson; + +namespace ZodSharp.Json; + +/// +/// Newtonsoft.Json counterpart of : the Newtonsoft package's JSON +/// Schema import types are available from the same project as the System.Text.Json package's types. +/// +public class NewtonsoftJsonImportTests +{ + const string ObjectSchema = + /*lang=json,strict*/ + """ + { + "type": "object", + "properties": { "name": { "type": "string", "minLength": 1 } }, + "required": ["name"] + } + """; + + [Test] + public async Task FromJsonSchema_GivenNewtonsoftJsonPackage_ValidatesData() + { + // Arrange + var schema = Z.FromJsonSchema(ObjectSchema); + + // Act + var validResult = schema.Validate(new Dictionary { ["name"] = "Ada" }); + var invalidResult = schema.Validate(new Dictionary { ["name"] = "" }); + + // Assert + await Assert.That(validResult.IsSuccess).IsTrue(); + await Assert.That(invalidResult.IsSuccess).IsFalse(); + } + + [Test] + public async Task ImportTypes_GivenBothJsonPackagesReferenced_ExposeDistinctFullNames() + { + // Arrange + var newtonsoftParser = typeof(FromJsonSchemaParser); + var systemTextJsonParser = typeof(ZodSharp.JsonSchema.SystemTextJson.FromJsonSchemaParser); + + // Act + var newtonsoftFullName = newtonsoftParser.FullName; + var systemTextJsonFullName = systemTextJsonParser.FullName; + + // Assert + await Assert.That(newtonsoftFullName).IsEqualTo("ZodSharp.JsonSchema.NewtonsoftJson.FromJsonSchemaParser"); + await Assert.That(systemTextJsonFullName).IsEqualTo("ZodSharp.JsonSchema.SystemTextJson.FromJsonSchemaParser"); + await Assert.That(newtonsoftParser).IsNotEqualTo(systemTextJsonParser); + } + + [Test] + public async Task JsonSchemaSerializerOptions_GivenBothJsonPackagesReferenced_BindToOwnSerializerType() + { + // Arrange + var newtonsoftOptions = JsonSchemaSerializerOptions.Default; + var systemTextJsonOptions = ZodSharp.JsonSchema.SystemTextJson.JsonSchemaSerializerOptions.Default; + + // Act + var newtonsoftOptionsType = newtonsoftOptions.GetType(); + var systemTextJsonOptionsType = systemTextJsonOptions.GetType(); + + // Assert + await Assert.That(newtonsoftOptionsType).IsEqualTo(typeof(Newtonsoft.Json.JsonSerializerSettings)); + await Assert.That(systemTextJsonOptionsType).IsEqualTo(typeof(System.Text.Json.JsonSerializerOptions)); + } +} diff --git a/src/tests/JsonInterop.UnitTests/Json/SystemTextJsonImportTests.cs b/src/tests/JsonInterop.UnitTests/Json/SystemTextJsonImportTests.cs new file mode 100644 index 0000000..e8bd2df --- /dev/null +++ b/src/tests/JsonInterop.UnitTests/Json/SystemTextJsonImportTests.cs @@ -0,0 +1,70 @@ +using ZodSharp.JsonSchema.SystemTextJson; + +namespace ZodSharp.Json; + +/// +/// Proves that Purview.ZodSharp.SystemTextJson and Purview.ZodSharp.NewtonsoftJson can be +/// referenced from the same project without extern alias: each package declares its JSON Schema +/// import types in a package-specific namespace, so the previously identical full names are now distinct. +/// Import exactly one JSON integration namespace per file. +/// +public class SystemTextJsonImportTests +{ + const string ObjectSchema = + /*lang=json,strict*/ + """ + { + "type": "object", + "properties": { "name": { "type": "string", "minLength": 1 } }, + "required": ["name"] + } + """; + + [Test] + public async Task FromJsonSchema_GivenSystemTextJsonPackage_ValidatesData() + { + // Arrange + var schema = Z.FromJsonSchema(ObjectSchema); + + // Act + var validResult = schema.Validate(new Dictionary { ["name"] = "Ada" }); + var invalidResult = schema.Validate(new Dictionary { ["name"] = "" }); + + // Assert + await Assert.That(validResult.IsSuccess).IsTrue(); + await Assert.That(invalidResult.IsSuccess).IsFalse(); + } + + [Test] + public async Task ImportTypes_GivenBothJsonPackagesReferenced_ExposeDistinctFullNames() + { + // Arrange + var systemTextJsonParser = typeof(FromJsonSchemaParser); + var newtonsoftParser = typeof(ZodSharp.JsonSchema.NewtonsoftJson.FromJsonSchemaParser); + + // Act + var systemTextJsonFullName = systemTextJsonParser.FullName; + var newtonsoftFullName = newtonsoftParser.FullName; + + // Assert + await Assert.That(systemTextJsonFullName).IsEqualTo("ZodSharp.JsonSchema.SystemTextJson.FromJsonSchemaParser"); + await Assert.That(newtonsoftFullName).IsEqualTo("ZodSharp.JsonSchema.NewtonsoftJson.FromJsonSchemaParser"); + await Assert.That(systemTextJsonParser).IsNotEqualTo(newtonsoftParser); + } + + [Test] + public async Task JsonSchemaSerializerOptions_GivenBothJsonPackagesReferenced_BindToOwnSerializerType() + { + // Arrange + var systemTextJsonOptions = JsonSchemaSerializerOptions.Default; + var newtonsoftOptions = ZodSharp.JsonSchema.NewtonsoftJson.JsonSchemaSerializerOptions.Default; + + // Act + var systemTextJsonOptionsType = systemTextJsonOptions.GetType(); + var newtonsoftOptionsType = newtonsoftOptions.GetType(); + + // Assert + await Assert.That(systemTextJsonOptionsType).IsEqualTo(typeof(System.Text.Json.JsonSerializerOptions)); + await Assert.That(newtonsoftOptionsType).IsEqualTo(typeof(Newtonsoft.Json.JsonSerializerSettings)); + } +} diff --git a/src/tests/JsonInterop.UnitTests/JsonInterop.UnitTests.csproj b/src/tests/JsonInterop.UnitTests/JsonInterop.UnitTests.csproj new file mode 100644 index 0000000..b46dbe0 --- /dev/null +++ b/src/tests/JsonInterop.UnitTests/JsonInterop.UnitTests.csproj @@ -0,0 +1,16 @@ + + + ZodSharp + Purview.ZodSharp.JsonInterop.UnitTests + $(NoWarn);CA1849;CA1002;CA2227; + + + + + + + + + diff --git a/src/tests/NewtonsoftJson.UnitTests/Json/Schema/FromJsonSchemaParserTests.cs b/src/tests/NewtonsoftJson.UnitTests/Json/Schema/FromJsonSchemaParserTests.cs new file mode 100644 index 0000000..e6ff327 --- /dev/null +++ b/src/tests/NewtonsoftJson.UnitTests/Json/Schema/FromJsonSchemaParserTests.cs @@ -0,0 +1,138 @@ +using ZodSharp.JsonSchema; +using ZodSharp.JsonSchema.NewtonsoftJson; + +namespace ZodSharp.Json.Schema; + +/// +/// Newtonsoft.Json counterpart of the System.Text.Json JSON Schema import tests: the +/// Z.FromJsonSchema extension member, local and external $ref handling, and the exported +/// serializer options for this package. +/// +public class FromJsonSchemaParserTests +{ + const string LocalRefSchema = + /*lang=json,strict*/ + """ + { + "$defs": { "name": { "type": "string", "minLength": 1 } }, + "type": "object", + "properties": { "name": { "$ref": "#/$defs/name" } }, + "required": ["name"] + } + """; + + const string ExternalRefSchema = + /*lang=json,strict*/ + """ + { + "type": "object", + "properties": { "name": { "$ref": "external.json#/$defs/name" } } + } + """; + + [Test] + public async Task FromJsonSchema_GivenLocalDefsRef_ResolvesReference() + { + // Arrange + var schema = Z.FromJsonSchema(LocalRefSchema); + + // Act + var validResult = schema.Validate(new Dictionary { ["name"] = "Ada" }); + var invalidResult = schema.Validate(new Dictionary { ["name"] = "" }); + + // Assert + await Assert.That(validResult.IsSuccess).IsTrue(); + await Assert.That(invalidResult.IsSuccess).IsFalse(); + } + + [Test] + public async Task FromJsonSchema_GivenExternalRef_ThrowsNotSupportedExceptionNamingReference() + { + // Arrange + + // Act + var exception = Assert.Throws(() => Z.FromJsonSchema(ExternalRefSchema)); + + // Assert + await Assert.That(exception!.Message).Contains("External $ref 'external.json#/$defs/name' is not supported"); + await Assert.That(exception.Message).Contains("pre-resolve external schemas before importing"); + } + + [Test] + public async Task Parse_GivenDefinitionWithStringConstraint_ValidatesAgainstConstraint() + { + // Arrange + JsonSchemaDefinition definition = new() { Type = "string", MinLength = 2 }; + + // Act + var schema = FromJsonSchemaParser.Parse(definition); + + // Assert + await Assert.That(schema.Validate("ab").IsSuccess).IsTrue(); + await Assert.That(schema.Validate("a").IsSuccess).IsFalse(); + } + + [Test] + public async Task Parse_GivenNullDefinition_ThrowsArgumentNullException() + { + // Arrange + JsonSchemaDefinition definition = null!; + + // Act + var exception = Assert.Throws(() => FromJsonSchemaParser.Parse(definition)); + + // Assert + await Assert.That(exception!.ParamName).IsEqualTo("schema"); + } + + [Test] + public async Task JsonSchemaSerializerOptions_Default_UsesJsonSchemaKeywordAndCamelCaseNames() + { + // Arrange + JsonSchemaDefinition definition = new() { MinLength = 3 }; + + // Act + var json = Newtonsoft.Json.JsonConvert.SerializeObject(definition, JsonSchemaSerializerOptions.Default); + + // Assert + await Assert.That(json).Contains("\"minLength\""); + await Assert + .That(JsonSchemaSerializerOptions.Default.Formatting) + .IsEqualTo(Newtonsoft.Json.Formatting.Indented); + } + + [Test] + public async Task Parse_GivenNullSubSchema_ThrowsArgumentException() + { + // Arrange + JsonSchemaDefinition definition = new() + { + Type = "object", + Properties = new Dictionary { ["name"] = null! }, + }; + + // Act + var exception = Assert.Throws(() => FromJsonSchemaParser.Parse(definition)); + + // Assert + await Assert.That(exception!.Message).Contains("null sub-schema"); + } + + [Test] + public async Task JsonSchemaSerializerOptions_GivenRefAndDefs_UsesJsonSchemaKeywordNames() + { + // Arrange + JsonSchemaDefinition definition = new() + { + Ref = "#/$defs/name", + Defs = new Dictionary { ["name"] = new() { Type = "string" } }, + }; + + // Act + var json = Newtonsoft.Json.JsonConvert.SerializeObject(definition, JsonSchemaSerializerOptions.Default); + + // Assert + await Assert.That(json).Contains("\"$ref\""); + await Assert.That(json).Contains("\"$defs\""); + } +} diff --git a/src/tests/SourceGenerators.UnitTests/AnalyzerReleaseTrackingTests.cs b/src/tests/SourceGenerators.UnitTests/AnalyzerReleaseTrackingTests.cs new file mode 100644 index 0000000..a8a3d5d --- /dev/null +++ b/src/tests/SourceGenerators.UnitTests/AnalyzerReleaseTrackingTests.cs @@ -0,0 +1,133 @@ +using System.Text.RegularExpressions; + +namespace ZodSharp.SourceGenerators; + +/// +/// Guards the analyzer release tracking files: every diagnostic that ships in the stable 2.0.0 release is +/// recorded in AnalyzerReleases.Shipped.md, the unshipped file declares no new rules, and each rule +/// identifier is tracked exactly once. These files drive the Roslyn release-tracking analyzers +/// (RS2000/RS2001), which report at build time when a diagnostic is missing from either file. +/// +public partial class AnalyzerReleaseTrackingTests +{ + static readonly string ReleaseDirectory = Path.Combine( + "..", + "..", + "..", + "..", + "..", + "..", + "src", + "src", + "SourceGenerators" + ); + + static readonly string[] ExpectedShippedRuleIds = + [ + "ZODSGEN001", + "ZODSGEN003", + "ZODSGEN004", + "ZODSGEN005", + "ZODSGEN006", + "ZODSGEN007", + "ZODSGEN008", + "ZODSGEN009", + "ZODSGEN010", + "ZODSGEN011", + "ZODSGEN012", + "ZODSGEN013", + "ZODSGEN014", + "ZODSGEN015", + "ZODSGEN016", + "ZODSGEN017", + "ZODSGEN018", + "ZODSGEN019", + "ZODSGEN020", + "ZODSGEN021", + "ZODSGEN027", + "ZODSGEN028", + "ZODSGEN029", + "ZODSGEN030", + "ZODSGEN031", + "ZODSGEN032", + "ZODSGEN033", + "ZODSGEN034", + "ZODSGEN035", + "ZODSGEN036", + "ZODSASP001", + "ZODSASP002", + "ZODSASP003", + "ZODSASP100", + "ZODSASP101", + ]; + + [Test] + public async Task ShippedReleases_GivenStableRelease_DeclareRelease2_0_0WithEveryDiagnosticId( + CancellationToken cancellationToken + ) + { + // Arrange + var markdown = await ReadReleaseFileAsync("AnalyzerReleases.Shipped.md", cancellationToken); + + // Act + var ruleIds = ReadRuleIds(markdown); + + // Assert + await Assert.That(markdown).Contains("## Release 2.0.0"); + await Assert.That(SortedRuleIds(ruleIds)).IsEqualTo(SortedRuleIds(ExpectedShippedRuleIds)); + } + + [Test] + public async Task UnshippedRelease_GivenStableRelease_DeclaresNoNewRules(CancellationToken cancellationToken) + { + // Arrange + var markdown = await ReadReleaseFileAsync("AnalyzerReleases.Unshipped.md", cancellationToken); + + // Act + var ruleIds = ReadRuleIds(markdown); + + // Assert + await Assert.That(markdown).Contains("### New Rules"); + await Assert.That(markdown).Contains("; Unshipped analyzer release"); + await Assert.That(ruleIds.Length).IsEqualTo(0); + } + + [Test] + public async Task ReleaseFiles_GivenAllDiagnostics_TrackEachRuleIdExactlyOnce(CancellationToken cancellationToken) + { + // Arrange + var shippedRuleIds = ReadRuleIds(await ReadReleaseFileAsync("AnalyzerReleases.Shipped.md", cancellationToken)); + var unshippedRuleIds = ReadRuleIds( + await ReadReleaseFileAsync("AnalyzerReleases.Unshipped.md", cancellationToken) + ); + + // Act + var duplicatedShippedRuleIds = shippedRuleIds + .GroupBy(static id => id, StringComparer.Ordinal) + .Where(static group => group.Count() > 1) + .Select(static group => group.Key) + .OrderBy(static id => id, StringComparer.Ordinal) + .ToArray(); + var rulesInBothFiles = shippedRuleIds + .Intersect(unshippedRuleIds, StringComparer.Ordinal) + .OrderBy(static id => id, StringComparer.Ordinal) + .ToArray(); + + // Assert + await Assert.That(string.Join(",", duplicatedShippedRuleIds)).IsEqualTo(string.Empty); + await Assert.That(string.Join(",", rulesInBothFiles)).IsEqualTo(string.Empty); + await Assert.That(shippedRuleIds.Length).IsEqualTo(ExpectedShippedRuleIds.Length); + } + + static async Task ReadReleaseFileAsync(string fileName, CancellationToken cancellationToken) => + await File.ReadAllTextAsync(Path.Combine(ReleaseDirectory, fileName), cancellationToken); + + static string[] ReadRuleIds(string markdown) => + [.. RuleIdRegex().Matches(markdown).Select(static match => match.Groups[1].Value)]; + + [GeneratedRegex(@"\|[ \t]*(ZODS[A-Z]{3}\d{3})[ \t]*\|")] + private static partial Regex RuleIdRegex(); + + static string SortedRuleIds(IEnumerable ruleIds) => + string.Join(",", ruleIds.Distinct(StringComparer.Ordinal).OrderBy(static id => id, StringComparer.Ordinal)); +} diff --git a/src/tests/SystemTextJson.UnitTests/Json/Schema/FromJsonSchemaParserTests.cs b/src/tests/SystemTextJson.UnitTests/Json/Schema/FromJsonSchemaParserTests.cs new file mode 100644 index 0000000..b0b4fdd --- /dev/null +++ b/src/tests/SystemTextJson.UnitTests/Json/Schema/FromJsonSchemaParserTests.cs @@ -0,0 +1,138 @@ +using ZodSharp.JsonSchema; +using ZodSharp.JsonSchema.SystemTextJson; + +namespace ZodSharp.Json.Schema; + +/// +/// Covers JSON Schema import through the System.Text.Json package: the Z.FromJsonSchema extension +/// member, local and external $ref handling, and the exported serializer options. +/// +public class FromJsonSchemaParserTests +{ + const string LocalRefSchema = + /*lang=json,strict*/ + """ + { + "$defs": { "name": { "type": "string", "minLength": 1 } }, + "type": "object", + "properties": { "name": { "$ref": "#/$defs/name" } }, + "required": ["name"] + } + """; + + const string ExternalRefSchema = + /*lang=json,strict*/ + """ + { + "type": "object", + "properties": { "name": { "$ref": "external.json#/$defs/name" } } + } + """; + + [Test] + public async Task FromJsonSchema_GivenLocalDefsRef_ResolvesReference() + { + // Arrange + var schema = Z.FromJsonSchema(LocalRefSchema); + + // Act + var validResult = schema.Validate(new Dictionary { ["name"] = "Ada" }); + var invalidResult = schema.Validate(new Dictionary { ["name"] = "" }); + + // Assert + await Assert.That(validResult.IsSuccess).IsTrue(); + await Assert.That(invalidResult.IsSuccess).IsFalse(); + } + + [Test] + public async Task FromJsonSchema_GivenExternalRef_ThrowsNotSupportedExceptionNamingReference() + { + // Arrange + + // Act + var exception = Assert.Throws(() => Z.FromJsonSchema(ExternalRefSchema)); + + // Assert + await Assert.That(exception!.Message).Contains("External $ref 'external.json#/$defs/name' is not supported"); + await Assert.That(exception.Message).Contains("pre-resolve external schemas before importing"); + } + + [Test] + public async Task Parse_GivenDefinitionWithStringConstraint_ValidatesAgainstConstraint() + { + // Arrange + JsonSchemaDefinition definition = new() { Type = "string", MinLength = 2 }; + + // Act + var schema = FromJsonSchemaParser.Parse(definition); + + // Assert + await Assert.That(schema.Validate("ab").IsSuccess).IsTrue(); + await Assert.That(schema.Validate("a").IsSuccess).IsFalse(); + } + + [Test] + public async Task Parse_GivenNullDefinition_ThrowsArgumentNullException() + { + // Arrange + JsonSchemaDefinition definition = null!; + + // Act + var exception = Assert.Throws(() => FromJsonSchemaParser.Parse(definition)); + + // Assert + await Assert.That(exception!.ParamName).IsEqualTo("schema"); + } + + [Test] + public async Task JsonSchemaSerializerOptions_Default_UsesJsonSchemaKeywordAndCamelCaseNames() + { + // Arrange + var namingPolicy = JsonSchemaSerializerOptions.Default.PropertyNamingPolicy; + + // Act + var refName = namingPolicy!.ConvertName(nameof(JsonSchemaDefinition.Ref)); + var defsName = namingPolicy.ConvertName(nameof(JsonSchemaDefinition.Defs)); + var minLengthName = namingPolicy.ConvertName(nameof(JsonSchemaDefinition.MinLength)); + + // Assert + await Assert.That(refName).IsEqualTo("$ref"); + await Assert.That(defsName).IsEqualTo("$defs"); + await Assert.That(minLengthName).IsEqualTo("minLength"); + } + + [Test] + public async Task Parse_GivenNullSubSchema_ThrowsArgumentException() + { + // Arrange + JsonSchemaDefinition definition = new() + { + Type = "object", + Properties = new Dictionary { ["name"] = null! }, + }; + + // Act + var exception = Assert.Throws(() => FromJsonSchemaParser.Parse(definition)); + + // Assert + await Assert.That(exception!.Message).Contains("null sub-schema"); + } + + [Test] + public async Task JsonSchemaSerializerOptions_GivenRefAndDefs_UsesJsonSchemaKeywordNames() + { + // Arrange + JsonSchemaDefinition definition = new() + { + Ref = "#/$defs/name", + Defs = new Dictionary { ["name"] = new() { Type = "string" } }, + }; + + // Act + var json = System.Text.Json.JsonSerializer.Serialize(definition, JsonSchemaSerializerOptions.Default); + + // Assert + await Assert.That(json).Contains("\"$ref\""); + await Assert.That(json).Contains("\"$defs\""); + } +} diff --git a/src/tests/ZodSharp.UnitTests/Core/MultiTargetingTests.cs b/src/tests/ZodSharp.UnitTests/Core/MultiTargetingTests.cs new file mode 100644 index 0000000..493d7c9 --- /dev/null +++ b/src/tests/ZodSharp.UnitTests/Core/MultiTargetingTests.cs @@ -0,0 +1,33 @@ +using System.Reflection; +using System.Runtime.Versioning; + +namespace ZodSharp.Core; + +/// +/// Verifies the multi-targeting guarantee for the core package: every test run resolves a +/// for the core assembly that matches one of the supported target +/// frameworks (net8.0, net9.0, net10.0). +/// +public class MultiTargetingTests +{ + static readonly string[] SupportedTargetFrameworkNames = + [ + ".NETCoreApp,Version=v8.0", + ".NETCoreApp,Version=v9.0", + ".NETCoreApp,Version=v10.0", + ]; + + [Test] + public async Task CoreAssembly_GivenMultiTargetedBuild_DeclaresSupportedTargetFramework() + { + // Arrange + var attribute = typeof(Z).Assembly.GetCustomAttribute(); + + // Act + var frameworkName = attribute?.FrameworkName; + + // Assert + await Assert.That(frameworkName).IsNotNull(); + await Assert.That(SupportedTargetFrameworkNames.Contains(frameworkName!, StringComparer.Ordinal)).IsTrue(); + } +}