diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a01320c4..1d7d8f9a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -67,6 +67,17 @@ jobs:
java-version: "21"
cache: gradle
+ - name: Set up .NET
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
+ with:
+ # The sample libraries target net8.0 and their test projects target
+ # net9.0, so both runtimes have to be present. `cargo test` also shells
+ # out to `dotnet build` from tests/generate_dotnet.rs, which relied on
+ # whatever SDK the runner happened to preinstall until this step.
+ dotnet-version: |
+ 8.0.x
+ 9.0.x
+
- name: Install Python dependencies (samples)
working-directory: samples/python
run: uv sync --locked
@@ -137,3 +148,11 @@ jobs:
- name: Run Java tests (advanced)
working-directory: advanced/samples/java
run: ./gradlew build --no-daemon
+
+ - name: Run .NET tests (samples)
+ working-directory: samples/dotnet
+ run: dotnet test tests/ --nologo
+
+ - name: Run .NET tests (advanced)
+ working-directory: advanced/samples/dotnet
+ run: dotnet test tests/ --nologo
diff --git a/advanced/samples/dotnet/json_schema/api/chat/Definitions.cs b/advanced/samples/dotnet/json_schema/api/chat/Definitions.cs
new file mode 100644
index 00000000..4c3ec031
--- /dev/null
+++ b/advanced/samples/dotnet/json_schema/api/chat/Definitions.cs
@@ -0,0 +1,288 @@
+//
+// Generated by nex-gen. DO NOT EDIT!
+#nullable enable
+#pragma warning disable CS1591
+
+using System.CodeDom.Compiler;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text.Json;
+
+namespace NexGen.ChatService
+{
+
+ ///
+ /// A single constraint failure. is the JSON member path
+ /// (dotted for nested members); is a human-readable
+ /// message naming the bound and the offending value.
+ ///
+ [GeneratedCode("nex-gen", null)]
+ public sealed class Violation
+ {
+ public Violation(string path, string reason)
+ {
+ Path = path;
+ Reason = reason;
+ }
+
+ public string Path { get; }
+
+ public string Reason { get; }
+
+ ///
+ /// Returns "Path: Reason", or just Reason when the path is
+ /// empty.
+ ///
+ public override string ToString() =>
+ Path.Length == 0 ? Reason : Path + ": " + Reason;
+ }
+
+ ///
+ /// Aggregates every found while (de)serializing a
+ /// value, surfacing them all in one error rather than stopping at the first.
+ ///
+ [GeneratedCode("nex-gen", null)]
+ public sealed class ValidationException : JsonException
+ {
+ public ValidationException(IReadOnlyList violations)
+ : base(FormatMessage(violations))
+ {
+ Violations = violations;
+ }
+
+ ///
+ /// Every violation found, never a partial first-failure.
+ ///
+ public IReadOnlyList Violations { get; }
+
+ private static string FormatMessage(IReadOnlyList violations)
+ {
+ var parts = new string[violations.Count];
+ for (var index = 0; index < violations.Count; index++)
+ {
+ parts[index] = violations[index].ToString();
+ }
+ return $"{violations.Count} validation error(s): {string.Join("; ", parts)}";
+ }
+ }
+
+ ///
+ /// Read helpers shared by every generated model. Internal because they are an
+ /// implementation detail of the generated (de)serialization path rather than
+ /// part of the contract surface.
+ ///
+ [GeneratedCode("nex-gen", null)]
+ internal static class JsonRuntime
+ {
+ ///
+ /// The largest integer a JSON number carries losslessly (2^53-1).
+ ///
+ /// Exceeding it is a **contract violation**, reported through
+ /// with the offending member's path — not
+ /// a parse failure. Mirrors Go's `integerCap`.
+ ///
+ internal const long IntegerCap = 9007199254740991L;
+
+ ///
+ /// Reads an optional member out of the extension-data bag, falling back to
+ /// when absent.
+ ///
+ internal static T? ReadOptionalValue(
+ IDictionary members,
+ string name,
+ T? defaultValue = default)
+ {
+ if (!members.TryGetValue(name, out var value))
+ {
+ return defaultValue;
+ }
+ return ReadJsonValue(value);
+ }
+
+ internal static T? ReadJsonValue(object? value)
+ {
+ if (value is null)
+ {
+ return default;
+ }
+ if (typeof(T) == typeof(long?) || typeof(T) == typeof(long))
+ {
+ return (T?)(object?)ReadJsonInteger(value);
+ }
+ if (value is JsonElement json)
+ {
+ return json.Deserialize();
+ }
+ if (value is T typed)
+ {
+ return typed;
+ }
+ return (T)value;
+ }
+
+ ///
+ /// Reads a JSON number as an integer, rejecting non-integral values and
+ /// anything beyond the lossless integer range.
+ ///
+ internal static long? ReadJsonInteger(object? value)
+ {
+ if (value is null)
+ {
+ return default;
+ }
+ if (value is JsonElement json)
+ {
+ if (json.ValueKind == JsonValueKind.Null)
+ {
+ return default;
+ }
+ if (json.ValueKind != JsonValueKind.Number)
+ {
+ throw new JsonException("expected integer");
+ }
+ // Exact across the whole Int64 range, and fails for a non-integral
+ // number. Deliberately does not enforce IntegerCap: a value past
+ // 2^53-1 is a constraint violation the validator reports with a
+ // path, not a parse error. Reading through double would round it
+ // away before the validator ever saw it.
+ if (json.TryGetInt64(out var exact))
+ {
+ return exact;
+ }
+ // A number spelled with a decimal point but no fractional part —
+ // `1.0` — is a valid integer per JSON Schema, and TryGetInt64
+ // rejects that spelling. Fall back to the double reading, bounded
+ // to the range where double to long is exact. `% 1 != 0` also
+ // rejects NaN and infinity, whose remainder is NaN.
+ if (json.TryGetDouble(out var number)
+ && number % 1 == 0
+ && number >= -9007199254740992d
+ && number <= 9007199254740992d)
+ {
+ return (long)number;
+ }
+ throw new JsonException("expected integer");
+ }
+ if (value is long longValue)
+ {
+ return longValue;
+ }
+ if (value is int intValue)
+ {
+ return intValue;
+ }
+ throw new JsonException("expected integer");
+ }
+
+ ///
+ /// Reports every uniqueItems duplicate, each against the index where
+ /// the value was first seen.
+ ///
+ /// A repeated value therefore yields one violation per later occurrence
+ /// rather than one per pair, which is what the other targets do.
+ ///
+ internal static void CollectDuplicateItems(
+ IReadOnlyList items,
+ string path,
+ List violations)
+ where T : notnull
+ {
+ var seen = new Dictionary(items.Count);
+ for (var index = 0; index < items.Count; index++)
+ {
+ if (seen.TryGetValue(items[index], out var first))
+ {
+ violations.Add(new Violation(
+ path,
+ $"duplicate items: element at index {index} equals index {first}"));
+ }
+ else
+ {
+ seen[items[index]] = index;
+ }
+ }
+ }
+
+ ///
+ /// Counts elements equal to a contains const value, feeding the
+ /// minContains/maxContains occurrence window.
+ ///
+ internal static int CountMatchingItems(IReadOnlyList items, T expected)
+ {
+ var comparer = EqualityComparer.Default;
+ var count = 0;
+ foreach (var item in items)
+ {
+ if (comparer.Equals(item, expected))
+ {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ ///
+ /// Counts Unicode code points, which is the unit JSON Schema's
+ /// minLength/maxLength measure.
+ ///
+ /// string.Length counts UTF-16 code units, so it would score an
+ /// astral character such as U+1F600 as 2 and reject a value the contract
+ /// permits. This matches Go's utf8.RuneCountInString and Java's
+ /// codePointCount, including counting an unpaired surrogate as one.
+ ///
+ internal static int CodePointCount(string value)
+ {
+ var count = 0;
+ for (var index = 0; index < value.Length; index++)
+ {
+ count++;
+ if (char.IsHighSurrogate(value[index])
+ && index + 1 < value.Length
+ && char.IsLowSurrogate(value[index + 1]))
+ {
+ index++;
+ }
+ }
+ return count;
+ }
+
+ ///
+ /// Quotes a string for a violation reason, mirroring Go's %q for the
+ /// values a contract admits. Used by the enum reason, which names the
+ /// offending value alongside the admitted set.
+ ///
+ internal static string Quote(string value) => "\"" + value + "\"";
+
+ ///
+ /// Joins a violation path prefix to a member name, so a nested model
+ /// reports page.blocks.order rather than a bare order.
+ ///
+ internal static string JoinPath(string prefix, string name) =>
+ prefix.Length == 0 ? name : prefix + "." + name;
+
+ ///
+ /// Renders a number for a violation reason using the invariant culture, so
+ /// the message never picks up a locale's decimal separator and stays
+ /// byte-identical to the other targets' diagnostics.
+ ///
+ internal static string FormatNumber(double value) =>
+ value.ToString(CultureInfo.InvariantCulture);
+
+ ///
+ internal static string FormatNumber(long value) =>
+ value.ToString(CultureInfo.InvariantCulture);
+
+ ///
+ /// Rejects an explicit JSON null for a member the contract declares
+ /// non-nullable.
+ ///
+ internal static void RejectNull(string name, object? value)
+ {
+ if (value is null || value is JsonElement { ValueKind: JsonValueKind.Null })
+ {
+ throw new JsonException($"{name}: explicit null not allowed");
+ }
+ }
+ }
+
+}
diff --git a/advanced/samples/dotnet/json_schema/api/chat/Models.cs b/advanced/samples/dotnet/json_schema/api/chat/Models.cs
index b454f0b3..519bd1aa 100644
--- a/advanced/samples/dotnet/json_schema/api/chat/Models.cs
+++ b/advanced/samples/dotnet/json_schema/api/chat/Models.cs
@@ -36,100 +36,44 @@ public class Labels : IJsonOnDeserialized
[JsonExtensionData]
public Dictionary AdditionalProperties { get; set; } = new Dictionary();
- void IJsonOnDeserialized.OnDeserialized()
- {
- if (AdditionalProperties.Count > 50)
- {
- throw new JsonException("maxProperties: at most 50 entries");
- }
- foreach (var entry in AdditionalProperties)
- {
- if (entry.Value is JsonElement json3 && json3.ValueKind != JsonValueKind.String)
- {
- throw new JsonException($"{entry.Key}: expected string");
- }
- else if (entry.Value is not JsonElement && entry.Value is not string)
- {
- throw new JsonException($"{entry.Key}: expected string");
- }
- }
- }
-
- private T? ReadOptionalValue(string name, T? defaultValue = default)
+ ///
+ /// Validates every constraint the contract declares on this type, throwing a
+ /// single carrying all violations rather
+ /// than stopping at the first.
+ ///
+ public void Validate()
{
- if (!AdditionalProperties.TryGetValue(name, out var value))
+ var violations = new List();
+ CollectViolations(violations, string.Empty);
+ if (violations.Count > 0)
{
- return defaultValue;
+ throw new ValidationException(violations);
}
- return ReadJsonValue(value);
}
- private static T? ReadJsonValue(object? value)
+ internal void CollectViolations(List violations, string path)
{
- if (value is null)
- {
- return default;
- }
- if (typeof(T) == typeof(long?) || typeof(T) == typeof(long))
- {
- return (T?)(object?)ReadJsonInteger(value);
- }
- if (value is JsonElement json)
- {
- return json.Deserialize();
- }
- if (value is T typed)
+ var propertyCount = AdditionalProperties.Count;
+ if (propertyCount > 50)
{
- return typed;
+ violations.Add(new Violation(path, "must have at most 50 properties, got " + propertyCount));
}
- return (T)value;
}
- private static long? ReadJsonInteger(object? value)
+ void IJsonOnDeserialized.OnDeserialized()
{
- const double maxSafeInteger = 9007199254740991d;
- if (value is null)
- {
- return default;
- }
- double number;
- if (value is JsonElement json)
+ foreach (var entry in AdditionalProperties)
{
- if (json.ValueKind == JsonValueKind.Null)
+ if (entry.Value is JsonElement json3 && json3.ValueKind != JsonValueKind.String)
{
- return default;
+ throw new JsonException($"{entry.Key}: expected string");
}
- if (json.ValueKind != JsonValueKind.Number)
+ else if (entry.Value is not JsonElement && entry.Value is not string)
{
- throw new JsonException("expected integer");
+ throw new JsonException($"{entry.Key}: expected string");
}
- number = json.GetDouble();
- }
- else if (value is long longValue)
- {
- number = longValue;
- }
- else if (value is int intValue)
- {
- number = intValue;
- }
- else
- {
- throw new JsonException("expected integer");
- }
- if (double.IsNaN(number) || double.IsInfinity(number) || Math.Truncate(number) != number || Math.Abs(number) > maxSafeInteger)
- {
- throw new JsonException("expected integer");
- }
- return (long)number;
- }
-
- private static void RejectNull(string name, object? value)
- {
- if (value is null || value is JsonElement { ValueKind: JsonValueKind.Null })
- {
- throw new JsonException($"{name}: explicit null not allowed");
}
+ Validate();
}
}
@@ -173,7 +117,7 @@ public string Kind
[JsonIgnore]
public string? ReplyToId
{
- get => ReadOptionalValue("replyToId");
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "replyToId");
init
{
AdditionalProperties["replyToId"] = value;
@@ -185,10 +129,10 @@ public string? ReplyToId
[JsonIgnore]
public long? Priority
{
- get => ReadOptionalValue("priority", 0);
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "priority", 0);
init
{
- RejectNull("priority", value);
+ JsonRuntime.RejectNull("priority", value);
AdditionalProperties["priority"] = value;
}
}
@@ -196,100 +140,50 @@ public long? Priority
[JsonExtensionData]
public Dictionary AdditionalProperties { get; set; } = new Dictionary();
- void IJsonOnDeserialized.OnDeserialized()
- {
- foreach (var key in AdditionalProperties.Keys)
- {
- if (key != "replyToId" && key != "priority")
- {
- throw new JsonException($"Unknown field `{key}`.");
- }
- }
- if (AdditionalProperties.TryGetValue("replyToId", out var replyToIdValue))
- {
- }
- if (AdditionalProperties.TryGetValue("priority", out var priorityValue))
- {
- RejectNull("priority", priorityValue);
- _ = ReadJsonValue(priorityValue);
- }
- }
-
- private T? ReadOptionalValue(string name, T? defaultValue = default)
+ ///
+ /// Validates every constraint the contract declares on this type, throwing a
+ /// single carrying all violations rather
+ /// than stopping at the first.
+ ///
+ public void Validate()
{
- if (!AdditionalProperties.TryGetValue(name, out var value))
+ var violations = new List();
+ CollectViolations(violations, string.Empty);
+ if (violations.Count > 0)
{
- return defaultValue;
+ throw new ValidationException(violations);
}
- return ReadJsonValue(value);
}
- private static T? ReadJsonValue(object? value)
+ internal void CollectViolations(List violations, string path)
{
- if (value is null)
- {
- return default;
- }
- if (typeof(T) == typeof(long?) || typeof(T) == typeof(long))
- {
- return (T?)(object?)ReadJsonInteger(value);
- }
- if (value is JsonElement json)
- {
- return json.Deserialize();
- }
- if (value is T typed)
+ if (Priority is long priorityValue)
{
- return typed;
+ if (priorityValue < -JsonRuntime.IntegerCap || priorityValue > JsonRuntime.IntegerCap)
+ {
+ violations.Add(new Violation(JsonRuntime.JoinPath(path, "priority"), "exceeds ±(2^53-1) integer cap"));
+ }
}
- return (T)value;
}
- private static long? ReadJsonInteger(object? value)
+ void IJsonOnDeserialized.OnDeserialized()
{
- const double maxSafeInteger = 9007199254740991d;
- if (value is null)
- {
- return default;
- }
- double number;
- if (value is JsonElement json)
+ foreach (var key in AdditionalProperties.Keys)
{
- if (json.ValueKind == JsonValueKind.Null)
- {
- return default;
- }
- if (json.ValueKind != JsonValueKind.Number)
+ if (key != "replyToId" && key != "priority")
{
- throw new JsonException("expected integer");
+ throw new JsonException($"Unknown field `{key}`.");
}
- number = json.GetDouble();
- }
- else if (value is long longValue)
- {
- number = longValue;
}
- else if (value is int intValue)
- {
- number = intValue;
- }
- else
- {
- throw new JsonException("expected integer");
- }
- if (double.IsNaN(number) || double.IsInfinity(number) || Math.Truncate(number) != number || Math.Abs(number) > maxSafeInteger)
+ if (AdditionalProperties.TryGetValue("replyToId", out var replyToIdValue))
{
- throw new JsonException("expected integer");
}
- return (long)number;
- }
-
- private static void RejectNull(string name, object? value)
- {
- if (value is null || value is JsonElement { ValueKind: JsonValueKind.Null })
+ if (AdditionalProperties.TryGetValue("priority", out var priorityValue))
{
- throw new JsonException($"{name}: explicit null not allowed");
+ JsonRuntime.RejectNull("priority", priorityValue);
+ _ = JsonRuntime.ReadJsonValue(priorityValue);
}
+ Validate();
}
}
@@ -322,20 +216,20 @@ public Room(string roomId, string displayName, string? topic)
[JsonIgnore]
public IReadOnlyList? Members
{
- get => ReadOptionalValue?>("members");
+ get => JsonRuntime.ReadOptionalValue?>(AdditionalProperties, "members");
init
{
- RejectNull("members", value);
+ JsonRuntime.RejectNull("members", value);
AdditionalProperties["members"] = value;
}
}
[JsonIgnore]
public Labels? Labels
{
- get => ReadOptionalValue("labels");
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "labels");
init
{
- RejectNull("labels", value);
+ JsonRuntime.RejectNull("labels", value);
AdditionalProperties["labels"] = value;
}
}
@@ -347,90 +241,13 @@ void IJsonOnDeserialized.OnDeserialized()
{
if (AdditionalProperties.TryGetValue("members", out var membersValue))
{
- RejectNull("members", membersValue);
- _ = ReadJsonValue?>(membersValue);
+ JsonRuntime.RejectNull("members", membersValue);
+ _ = JsonRuntime.ReadJsonValue?>(membersValue);
}
if (AdditionalProperties.TryGetValue("labels", out var labelsValue))
{
- RejectNull("labels", labelsValue);
- _ = ReadJsonValue(labelsValue);
- }
- }
-
- private T? ReadOptionalValue(string name, T? defaultValue = default)
- {
- if (!AdditionalProperties.TryGetValue(name, out var value))
- {
- return defaultValue;
- }
- return ReadJsonValue(value);
- }
-
- private static T? ReadJsonValue(object? value)
- {
- if (value is null)
- {
- return default;
- }
- if (typeof(T) == typeof(long?) || typeof(T) == typeof(long))
- {
- return (T?)(object?)ReadJsonInteger(value);
- }
- if (value is JsonElement json)
- {
- return json.Deserialize();
- }
- if (value is T typed)
- {
- return typed;
- }
- return (T)value;
- }
-
- private static long? ReadJsonInteger(object? value)
- {
- const double maxSafeInteger = 9007199254740991d;
- if (value is null)
- {
- return default;
- }
- double number;
- if (value is JsonElement json)
- {
- if (json.ValueKind == JsonValueKind.Null)
- {
- return default;
- }
- if (json.ValueKind != JsonValueKind.Number)
- {
- throw new JsonException("expected integer");
- }
- number = json.GetDouble();
- }
- else if (value is long longValue)
- {
- number = longValue;
- }
- else if (value is int intValue)
- {
- number = intValue;
- }
- else
- {
- throw new JsonException("expected integer");
- }
- if (double.IsNaN(number) || double.IsInfinity(number) || Math.Truncate(number) != number || Math.Abs(number) > maxSafeInteger)
- {
- throw new JsonException("expected integer");
- }
- return (long)number;
- }
-
- private static void RejectNull(string name, object? value)
- {
- if (value is null || value is JsonElement { ValueKind: JsonValueKind.Null })
- {
- throw new JsonException($"{name}: explicit null not allowed");
+ JsonRuntime.RejectNull("labels", labelsValue);
+ _ = JsonRuntime.ReadJsonValue(labelsValue);
}
}
}
diff --git a/advanced/samples/dotnet/json_schema/api/kb/Definitions.cs b/advanced/samples/dotnet/json_schema/api/kb/Definitions.cs
new file mode 100644
index 00000000..6688a8ae
--- /dev/null
+++ b/advanced/samples/dotnet/json_schema/api/kb/Definitions.cs
@@ -0,0 +1,288 @@
+//
+// Generated by nex-gen. DO NOT EDIT!
+#nullable enable
+#pragma warning disable CS1591
+
+using System.CodeDom.Compiler;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text.Json;
+
+namespace NexGen.Generated
+{
+
+ ///
+ /// A single constraint failure. is the JSON member path
+ /// (dotted for nested members); is a human-readable
+ /// message naming the bound and the offending value.
+ ///
+ [GeneratedCode("nex-gen", null)]
+ public sealed class Violation
+ {
+ public Violation(string path, string reason)
+ {
+ Path = path;
+ Reason = reason;
+ }
+
+ public string Path { get; }
+
+ public string Reason { get; }
+
+ ///
+ /// Returns "Path: Reason", or just Reason when the path is
+ /// empty.
+ ///
+ public override string ToString() =>
+ Path.Length == 0 ? Reason : Path + ": " + Reason;
+ }
+
+ ///
+ /// Aggregates every found while (de)serializing a
+ /// value, surfacing them all in one error rather than stopping at the first.
+ ///
+ [GeneratedCode("nex-gen", null)]
+ public sealed class ValidationException : JsonException
+ {
+ public ValidationException(IReadOnlyList violations)
+ : base(FormatMessage(violations))
+ {
+ Violations = violations;
+ }
+
+ ///
+ /// Every violation found, never a partial first-failure.
+ ///
+ public IReadOnlyList Violations { get; }
+
+ private static string FormatMessage(IReadOnlyList violations)
+ {
+ var parts = new string[violations.Count];
+ for (var index = 0; index < violations.Count; index++)
+ {
+ parts[index] = violations[index].ToString();
+ }
+ return $"{violations.Count} validation error(s): {string.Join("; ", parts)}";
+ }
+ }
+
+ ///
+ /// Read helpers shared by every generated model. Internal because they are an
+ /// implementation detail of the generated (de)serialization path rather than
+ /// part of the contract surface.
+ ///
+ [GeneratedCode("nex-gen", null)]
+ internal static class JsonRuntime
+ {
+ ///
+ /// The largest integer a JSON number carries losslessly (2^53-1).
+ ///
+ /// Exceeding it is a **contract violation**, reported through
+ /// with the offending member's path — not
+ /// a parse failure. Mirrors Go's `integerCap`.
+ ///
+ internal const long IntegerCap = 9007199254740991L;
+
+ ///
+ /// Reads an optional member out of the extension-data bag, falling back to
+ /// when absent.
+ ///
+ internal static T? ReadOptionalValue(
+ IDictionary members,
+ string name,
+ T? defaultValue = default)
+ {
+ if (!members.TryGetValue(name, out var value))
+ {
+ return defaultValue;
+ }
+ return ReadJsonValue(value);
+ }
+
+ internal static T? ReadJsonValue(object? value)
+ {
+ if (value is null)
+ {
+ return default;
+ }
+ if (typeof(T) == typeof(long?) || typeof(T) == typeof(long))
+ {
+ return (T?)(object?)ReadJsonInteger(value);
+ }
+ if (value is JsonElement json)
+ {
+ return json.Deserialize();
+ }
+ if (value is T typed)
+ {
+ return typed;
+ }
+ return (T)value;
+ }
+
+ ///
+ /// Reads a JSON number as an integer, rejecting non-integral values and
+ /// anything beyond the lossless integer range.
+ ///
+ internal static long? ReadJsonInteger(object? value)
+ {
+ if (value is null)
+ {
+ return default;
+ }
+ if (value is JsonElement json)
+ {
+ if (json.ValueKind == JsonValueKind.Null)
+ {
+ return default;
+ }
+ if (json.ValueKind != JsonValueKind.Number)
+ {
+ throw new JsonException("expected integer");
+ }
+ // Exact across the whole Int64 range, and fails for a non-integral
+ // number. Deliberately does not enforce IntegerCap: a value past
+ // 2^53-1 is a constraint violation the validator reports with a
+ // path, not a parse error. Reading through double would round it
+ // away before the validator ever saw it.
+ if (json.TryGetInt64(out var exact))
+ {
+ return exact;
+ }
+ // A number spelled with a decimal point but no fractional part —
+ // `1.0` — is a valid integer per JSON Schema, and TryGetInt64
+ // rejects that spelling. Fall back to the double reading, bounded
+ // to the range where double to long is exact. `% 1 != 0` also
+ // rejects NaN and infinity, whose remainder is NaN.
+ if (json.TryGetDouble(out var number)
+ && number % 1 == 0
+ && number >= -9007199254740992d
+ && number <= 9007199254740992d)
+ {
+ return (long)number;
+ }
+ throw new JsonException("expected integer");
+ }
+ if (value is long longValue)
+ {
+ return longValue;
+ }
+ if (value is int intValue)
+ {
+ return intValue;
+ }
+ throw new JsonException("expected integer");
+ }
+
+ ///
+ /// Reports every uniqueItems duplicate, each against the index where
+ /// the value was first seen.
+ ///
+ /// A repeated value therefore yields one violation per later occurrence
+ /// rather than one per pair, which is what the other targets do.
+ ///
+ internal static void CollectDuplicateItems(
+ IReadOnlyList items,
+ string path,
+ List violations)
+ where T : notnull
+ {
+ var seen = new Dictionary(items.Count);
+ for (var index = 0; index < items.Count; index++)
+ {
+ if (seen.TryGetValue(items[index], out var first))
+ {
+ violations.Add(new Violation(
+ path,
+ $"duplicate items: element at index {index} equals index {first}"));
+ }
+ else
+ {
+ seen[items[index]] = index;
+ }
+ }
+ }
+
+ ///
+ /// Counts elements equal to a contains const value, feeding the
+ /// minContains/maxContains occurrence window.
+ ///
+ internal static int CountMatchingItems(IReadOnlyList items, T expected)
+ {
+ var comparer = EqualityComparer.Default;
+ var count = 0;
+ foreach (var item in items)
+ {
+ if (comparer.Equals(item, expected))
+ {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ ///
+ /// Counts Unicode code points, which is the unit JSON Schema's
+ /// minLength/maxLength measure.
+ ///
+ /// string.Length counts UTF-16 code units, so it would score an
+ /// astral character such as U+1F600 as 2 and reject a value the contract
+ /// permits. This matches Go's utf8.RuneCountInString and Java's
+ /// codePointCount, including counting an unpaired surrogate as one.
+ ///
+ internal static int CodePointCount(string value)
+ {
+ var count = 0;
+ for (var index = 0; index < value.Length; index++)
+ {
+ count++;
+ if (char.IsHighSurrogate(value[index])
+ && index + 1 < value.Length
+ && char.IsLowSurrogate(value[index + 1]))
+ {
+ index++;
+ }
+ }
+ return count;
+ }
+
+ ///
+ /// Quotes a string for a violation reason, mirroring Go's %q for the
+ /// values a contract admits. Used by the enum reason, which names the
+ /// offending value alongside the admitted set.
+ ///
+ internal static string Quote(string value) => "\"" + value + "\"";
+
+ ///
+ /// Joins a violation path prefix to a member name, so a nested model
+ /// reports page.blocks.order rather than a bare order.
+ ///
+ internal static string JoinPath(string prefix, string name) =>
+ prefix.Length == 0 ? name : prefix + "." + name;
+
+ ///
+ /// Renders a number for a violation reason using the invariant culture, so
+ /// the message never picks up a locale's decimal separator and stays
+ /// byte-identical to the other targets' diagnostics.
+ ///
+ internal static string FormatNumber(double value) =>
+ value.ToString(CultureInfo.InvariantCulture);
+
+ ///
+ internal static string FormatNumber(long value) =>
+ value.ToString(CultureInfo.InvariantCulture);
+
+ ///
+ /// Rejects an explicit JSON null for a member the contract declares
+ /// non-nullable.
+ ///
+ internal static void RejectNull(string name, object? value)
+ {
+ if (value is null || value is JsonElement { ValueKind: JsonValueKind.Null })
+ {
+ throw new JsonException($"{name}: explicit null not allowed");
+ }
+ }
+ }
+
+}
diff --git a/advanced/samples/dotnet/json_schema/api/kb/content/block/Models.cs b/advanced/samples/dotnet/json_schema/api/kb/content/block/Models.cs
index 7833d1f2..6769ddc8 100644
--- a/advanced/samples/dotnet/json_schema/api/kb/content/block/Models.cs
+++ b/advanced/samples/dotnet/json_schema/api/kb/content/block/Models.cs
@@ -37,20 +37,20 @@ public Block(string blockId, long order)
[JsonIgnore]
public string? Text
{
- get => ReadOptionalValue("text");
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "text");
init
{
- RejectNull("text", value);
+ JsonRuntime.RejectNull("text", value);
AdditionalProperties["text"] = value;
}
}
[JsonIgnore]
public global::NexGen.Generated.Content.Block.BlockStyle? Style
{
- get => ReadOptionalValue("style");
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "style");
init
{
- RejectNull("style", value);
+ JsonRuntime.RejectNull("style", value);
AdditionalProperties["style"] = value;
}
}
@@ -60,7 +60,7 @@ public string? Text
[JsonIgnore]
public global::NexGen.Generated.Content.Page.Page? Page
{
- get => ReadOptionalValue("page");
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "page");
init
{
AdditionalProperties["page"] = value;
@@ -70,6 +70,33 @@ public string? Text
[JsonExtensionData]
public Dictionary AdditionalProperties { get; set; } = new Dictionary();
+ ///
+ /// Validates every constraint the contract declares on this type, throwing a
+ /// single carrying all violations rather
+ /// than stopping at the first.
+ ///
+ public void Validate()
+ {
+ var violations = new List();
+ CollectViolations(violations, string.Empty);
+ if (violations.Count > 0)
+ {
+ throw new ValidationException(violations);
+ }
+ }
+
+ internal void CollectViolations(List violations, string path)
+ {
+ if (Order < -JsonRuntime.IntegerCap || Order > JsonRuntime.IntegerCap)
+ {
+ violations.Add(new Violation(JsonRuntime.JoinPath(path, "order"), "exceeds ±(2^53-1) integer cap"));
+ }
+ if (Order < 0)
+ {
+ violations.Add(new Violation(JsonRuntime.JoinPath(path, "order"), "must be >= 0, got " + JsonRuntime.FormatNumber(Order)));
+ }
+ }
+
void IJsonOnDeserialized.OnDeserialized()
{
foreach (var key in AdditionalProperties.Keys)
@@ -81,7 +108,7 @@ void IJsonOnDeserialized.OnDeserialized()
}
if (AdditionalProperties.TryGetValue("text", out var textValue))
{
- RejectNull("text", textValue);
+ JsonRuntime.RejectNull("text", textValue);
if (textValue is JsonElement json3 && json3.ValueKind != JsonValueKind.String)
{
throw new JsonException($"{"text"}: expected string");
@@ -93,89 +120,13 @@ void IJsonOnDeserialized.OnDeserialized()
}
if (AdditionalProperties.TryGetValue("style", out var styleValue))
{
- RejectNull("style", styleValue);
- _ = ReadJsonValue(styleValue);
+ JsonRuntime.RejectNull("style", styleValue);
+ _ = JsonRuntime.ReadJsonValue(styleValue);
}
if (AdditionalProperties.TryGetValue("page", out var pageValue))
{
}
- }
-
- private T? ReadOptionalValue(string name, T? defaultValue = default)
- {
- if (!AdditionalProperties.TryGetValue(name, out var value))
- {
- return defaultValue;
- }
- return ReadJsonValue(value);
- }
-
- private static T? ReadJsonValue(object? value)
- {
- if (value is null)
- {
- return default;
- }
- if (typeof(T) == typeof(long?) || typeof(T) == typeof(long))
- {
- return (T?)(object?)ReadJsonInteger(value);
- }
- if (value is JsonElement json)
- {
- return json.Deserialize();
- }
- if (value is T typed)
- {
- return typed;
- }
- return (T)value;
- }
-
- private static long? ReadJsonInteger(object? value)
- {
- const double maxSafeInteger = 9007199254740991d;
- if (value is null)
- {
- return default;
- }
- double number;
- if (value is JsonElement json)
- {
- if (json.ValueKind == JsonValueKind.Null)
- {
- return default;
- }
- if (json.ValueKind != JsonValueKind.Number)
- {
- throw new JsonException("expected integer");
- }
- number = json.GetDouble();
- }
- else if (value is long longValue)
- {
- number = longValue;
- }
- else if (value is int intValue)
- {
- number = intValue;
- }
- else
- {
- throw new JsonException("expected integer");
- }
- if (double.IsNaN(number) || double.IsInfinity(number) || Math.Truncate(number) != number || Math.Abs(number) > maxSafeInteger)
- {
- throw new JsonException("expected integer");
- }
- return (long)number;
- }
-
- private static void RejectNull(string name, object? value)
- {
- if (value is null || value is JsonElement { ValueKind: JsonValueKind.Null })
- {
- throw new JsonException($"{name}: explicit null not allowed");
- }
+ Validate();
}
}
@@ -189,20 +140,20 @@ public class BlockStyle : IJsonOnDeserialized
[JsonIgnore]
public bool? Bold
{
- get => ReadOptionalValue("bold");
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "bold");
init
{
- RejectNull("bold", value);
+ JsonRuntime.RejectNull("bold", value);
AdditionalProperties["bold"] = value;
}
}
[JsonIgnore]
public long? Indent
{
- get => ReadOptionalValue("indent");
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "indent");
init
{
- RejectNull("indent", value);
+ JsonRuntime.RejectNull("indent", value);
AdditionalProperties["indent"] = value;
}
}
@@ -210,101 +161,55 @@ public long? Indent
[JsonExtensionData]
public Dictionary AdditionalProperties { get; set; } = new Dictionary();
- void IJsonOnDeserialized.OnDeserialized()
- {
- foreach (var key in AdditionalProperties.Keys)
- {
- if (key != "bold" && key != "indent")
- {
- throw new JsonException($"Unknown field `{key}`.");
- }
- }
- if (AdditionalProperties.TryGetValue("bold", out var boldValue))
- {
- RejectNull("bold", boldValue);
- }
- if (AdditionalProperties.TryGetValue("indent", out var indentValue))
- {
- RejectNull("indent", indentValue);
- _ = ReadJsonValue(indentValue);
- }
- }
-
- private T? ReadOptionalValue(string name, T? defaultValue = default)
+ ///
+ /// Validates every constraint the contract declares on this type, throwing a
+ /// single carrying all violations rather
+ /// than stopping at the first.
+ ///
+ public void Validate()
{
- if (!AdditionalProperties.TryGetValue(name, out var value))
+ var violations = new List();
+ CollectViolations(violations, string.Empty);
+ if (violations.Count > 0)
{
- return defaultValue;
+ throw new ValidationException(violations);
}
- return ReadJsonValue(value);
}
- private static T? ReadJsonValue(object? value)
+ internal void CollectViolations(List violations, string path)
{
- if (value is null)
- {
- return default;
- }
- if (typeof(T) == typeof(long?) || typeof(T) == typeof(long))
- {
- return (T?)(object?)ReadJsonInteger(value);
- }
- if (value is JsonElement json)
+ if (Indent is long indentValue)
{
- return json.Deserialize();
- }
- if (value is T typed)
- {
- return typed;
+ if (indentValue < -JsonRuntime.IntegerCap || indentValue > JsonRuntime.IntegerCap)
+ {
+ violations.Add(new Violation(JsonRuntime.JoinPath(path, "indent"), "exceeds ±(2^53-1) integer cap"));
+ }
+ if (indentValue < 0)
+ {
+ violations.Add(new Violation(JsonRuntime.JoinPath(path, "indent"), "must be >= 0, got " + JsonRuntime.FormatNumber(indentValue)));
+ }
}
- return (T)value;
}
- private static long? ReadJsonInteger(object? value)
+ void IJsonOnDeserialized.OnDeserialized()
{
- const double maxSafeInteger = 9007199254740991d;
- if (value is null)
- {
- return default;
- }
- double number;
- if (value is JsonElement json)
+ foreach (var key in AdditionalProperties.Keys)
{
- if (json.ValueKind == JsonValueKind.Null)
- {
- return default;
- }
- if (json.ValueKind != JsonValueKind.Number)
+ if (key != "bold" && key != "indent")
{
- throw new JsonException("expected integer");
+ throw new JsonException($"Unknown field `{key}`.");
}
- number = json.GetDouble();
- }
- else if (value is long longValue)
- {
- number = longValue;
- }
- else if (value is int intValue)
- {
- number = intValue;
- }
- else
- {
- throw new JsonException("expected integer");
}
- if (double.IsNaN(number) || double.IsInfinity(number) || Math.Truncate(number) != number || Math.Abs(number) > maxSafeInteger)
+ if (AdditionalProperties.TryGetValue("bold", out var boldValue))
{
- throw new JsonException("expected integer");
+ JsonRuntime.RejectNull("bold", boldValue);
}
- return (long)number;
- }
-
- private static void RejectNull(string name, object? value)
- {
- if (value is null || value is JsonElement { ValueKind: JsonValueKind.Null })
+ if (AdditionalProperties.TryGetValue("indent", out var indentValue))
{
- throw new JsonException($"{name}: explicit null not allowed");
+ JsonRuntime.RejectNull("indent", indentValue);
+ _ = JsonRuntime.ReadJsonValue(indentValue);
}
+ Validate();
}
}
diff --git a/advanced/samples/dotnet/json_schema/api/kb/content/page/Models.cs b/advanced/samples/dotnet/json_schema/api/kb/content/page/Models.cs
index 9e1798b4..8b51b659 100644
--- a/advanced/samples/dotnet/json_schema/api/kb/content/page/Models.cs
+++ b/advanced/samples/dotnet/json_schema/api/kb/content/page/Models.cs
@@ -41,10 +41,10 @@ public Page(string pageId, string title, global::NexGen.Generated.Content.Page.P
[JsonIgnore]
public IReadOnlyList? Blocks
{
- get => ReadOptionalValue?>("blocks");
+ get => JsonRuntime.ReadOptionalValue?>(AdditionalProperties, "blocks");
init
{
- RejectNull("blocks", value);
+ JsonRuntime.RejectNull("blocks", value);
AdditionalProperties["blocks"] = value;
}
}
@@ -63,85 +63,8 @@ void IJsonOnDeserialized.OnDeserialized()
}
if (AdditionalProperties.TryGetValue("blocks", out var blocksValue))
{
- RejectNull("blocks", blocksValue);
- _ = ReadJsonValue?>(blocksValue);
- }
- }
-
- private T? ReadOptionalValue(string name, T? defaultValue = default)
- {
- if (!AdditionalProperties.TryGetValue(name, out var value))
- {
- return defaultValue;
- }
- return ReadJsonValue(value);
- }
-
- private static T? ReadJsonValue(object? value)
- {
- if (value is null)
- {
- return default;
- }
- if (typeof(T) == typeof(long?) || typeof(T) == typeof(long))
- {
- return (T?)(object?)ReadJsonInteger(value);
- }
- if (value is JsonElement json)
- {
- return json.Deserialize();
- }
- if (value is T typed)
- {
- return typed;
- }
- return (T)value;
- }
-
- private static long? ReadJsonInteger(object? value)
- {
- const double maxSafeInteger = 9007199254740991d;
- if (value is null)
- {
- return default;
- }
- double number;
- if (value is JsonElement json)
- {
- if (json.ValueKind == JsonValueKind.Null)
- {
- return default;
- }
- if (json.ValueKind != JsonValueKind.Number)
- {
- throw new JsonException("expected integer");
- }
- number = json.GetDouble();
- }
- else if (value is long longValue)
- {
- number = longValue;
- }
- else if (value is int intValue)
- {
- number = intValue;
- }
- else
- {
- throw new JsonException("expected integer");
- }
- if (double.IsNaN(number) || double.IsInfinity(number) || Math.Truncate(number) != number || Math.Abs(number) > maxSafeInteger)
- {
- throw new JsonException("expected integer");
- }
- return (long)number;
- }
-
- private static void RejectNull(string name, object? value)
- {
- if (value is null || value is JsonElement { ValueKind: JsonValueKind.Null })
- {
- throw new JsonException($"{name}: explicit null not allowed");
+ JsonRuntime.RejectNull("blocks", blocksValue);
+ _ = JsonRuntime.ReadJsonValue?>(blocksValue);
}
}
}
@@ -164,10 +87,10 @@ public PageMeta(string author)
[JsonIgnore]
public long? WordCount
{
- get => ReadOptionalValue("wordCount");
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "wordCount");
init
{
- RejectNull("wordCount", value);
+ JsonRuntime.RejectNull("wordCount", value);
AdditionalProperties["wordCount"] = value;
}
}
@@ -175,97 +98,47 @@ public long? WordCount
[JsonExtensionData]
public Dictionary AdditionalProperties { get; set; } = new Dictionary();
- void IJsonOnDeserialized.OnDeserialized()
- {
- foreach (var key in AdditionalProperties.Keys)
- {
- if (key != "wordCount")
- {
- throw new JsonException($"Unknown field `{key}`.");
- }
- }
- if (AdditionalProperties.TryGetValue("wordCount", out var wordCountValue))
- {
- RejectNull("wordCount", wordCountValue);
- _ = ReadJsonValue(wordCountValue);
- }
- }
-
- private T? ReadOptionalValue(string name, T? defaultValue = default)
+ ///
+ /// Validates every constraint the contract declares on this type, throwing a
+ /// single carrying all violations rather
+ /// than stopping at the first.
+ ///
+ public void Validate()
{
- if (!AdditionalProperties.TryGetValue(name, out var value))
+ var violations = new List();
+ CollectViolations(violations, string.Empty);
+ if (violations.Count > 0)
{
- return defaultValue;
+ throw new ValidationException(violations);
}
- return ReadJsonValue(value);
}
- private static T? ReadJsonValue(object? value)
+ internal void CollectViolations(List violations, string path)
{
- if (value is null)
- {
- return default;
- }
- if (typeof(T) == typeof(long?) || typeof(T) == typeof(long))
+ if (WordCount is long wordCountValue)
{
- return (T?)(object?)ReadJsonInteger(value);
- }
- if (value is JsonElement json)
- {
- return json.Deserialize();
- }
- if (value is T typed)
- {
- return typed;
+ if (wordCountValue < -JsonRuntime.IntegerCap || wordCountValue > JsonRuntime.IntegerCap)
+ {
+ violations.Add(new Violation(JsonRuntime.JoinPath(path, "wordCount"), "exceeds ±(2^53-1) integer cap"));
+ }
}
- return (T)value;
}
- private static long? ReadJsonInteger(object? value)
+ void IJsonOnDeserialized.OnDeserialized()
{
- const double maxSafeInteger = 9007199254740991d;
- if (value is null)
- {
- return default;
- }
- double number;
- if (value is JsonElement json)
+ foreach (var key in AdditionalProperties.Keys)
{
- if (json.ValueKind == JsonValueKind.Null)
- {
- return default;
- }
- if (json.ValueKind != JsonValueKind.Number)
+ if (key != "wordCount")
{
- throw new JsonException("expected integer");
+ throw new JsonException($"Unknown field `{key}`.");
}
- number = json.GetDouble();
- }
- else if (value is long longValue)
- {
- number = longValue;
}
- else if (value is int intValue)
- {
- number = intValue;
- }
- else
- {
- throw new JsonException("expected integer");
- }
- if (double.IsNaN(number) || double.IsInfinity(number) || Math.Truncate(number) != number || Math.Abs(number) > maxSafeInteger)
- {
- throw new JsonException("expected integer");
- }
- return (long)number;
- }
-
- private static void RejectNull(string name, object? value)
- {
- if (value is null || value is JsonElement { ValueKind: JsonValueKind.Null })
+ if (AdditionalProperties.TryGetValue("wordCount", out var wordCountValue))
{
- throw new JsonException($"{name}: explicit null not allowed");
+ JsonRuntime.RejectNull("wordCount", wordCountValue);
+ _ = JsonRuntime.ReadJsonValue(wordCountValue);
}
+ Validate();
}
}
diff --git a/advanced/samples/dotnet/json_schema/api/kb/kb/Models.cs b/advanced/samples/dotnet/json_schema/api/kb/kb/Models.cs
index e8703570..8fa214bf 100644
--- a/advanced/samples/dotnet/json_schema/api/kb/kb/Models.cs
+++ b/advanced/samples/dotnet/json_schema/api/kb/kb/Models.cs
@@ -47,7 +47,7 @@ public GetPageInput(string pageId)
[JsonUnmappedMemberHandling(JsonUnmappedMemberHandling.Disallow)]
[GeneratedCode("nex-gen", null)]
- public class PutBlockOutput
+ public class PutBlockOutput : IJsonOnDeserialized
{
public PutBlockOutput(string blockId, long revision)
{
@@ -61,6 +61,34 @@ public PutBlockOutput(string blockId, long revision)
[JsonPropertyName("revision")]
[JsonRequired]
public long Revision { get; init; }
+
+ ///
+ /// Validates every constraint the contract declares on this type, throwing a
+ /// single carrying all violations rather
+ /// than stopping at the first.
+ ///
+ public void Validate()
+ {
+ var violations = new List();
+ CollectViolations(violations, string.Empty);
+ if (violations.Count > 0)
+ {
+ throw new ValidationException(violations);
+ }
+ }
+
+ internal void CollectViolations(List violations, string path)
+ {
+ if (Revision < -JsonRuntime.IntegerCap || Revision > JsonRuntime.IntegerCap)
+ {
+ violations.Add(new Violation(JsonRuntime.JoinPath(path, "revision"), "exceeds ±(2^53-1) integer cap"));
+ }
+ }
+
+ void IJsonOnDeserialized.OnDeserialized()
+ {
+ Validate();
+ }
}
}
diff --git a/advanced/samples/dotnet/json_schema/api/kb/tree/category/Models.cs b/advanced/samples/dotnet/json_schema/api/kb/tree/category/Models.cs
index 51ff44bc..7e8738a1 100644
--- a/advanced/samples/dotnet/json_schema/api/kb/tree/category/Models.cs
+++ b/advanced/samples/dotnet/json_schema/api/kb/tree/category/Models.cs
@@ -36,10 +36,10 @@ public Category(string id, string name)
[JsonIgnore]
public IReadOnlyList? Children
{
- get => ReadOptionalValue?>("children");
+ get => JsonRuntime.ReadOptionalValue?>(AdditionalProperties, "children");
init
{
- RejectNull("children", value);
+ JsonRuntime.RejectNull("children", value);
AdditionalProperties["children"] = value;
}
}
@@ -58,85 +58,8 @@ void IJsonOnDeserialized.OnDeserialized()
}
if (AdditionalProperties.TryGetValue("children", out var childrenValue))
{
- RejectNull("children", childrenValue);
- _ = ReadJsonValue?>(childrenValue);
- }
- }
-
- private T? ReadOptionalValue(string name, T? defaultValue = default)
- {
- if (!AdditionalProperties.TryGetValue(name, out var value))
- {
- return defaultValue;
- }
- return ReadJsonValue(value);
- }
-
- private static T? ReadJsonValue(object? value)
- {
- if (value is null)
- {
- return default;
- }
- if (typeof(T) == typeof(long?) || typeof(T) == typeof(long))
- {
- return (T?)(object?)ReadJsonInteger(value);
- }
- if (value is JsonElement json)
- {
- return json.Deserialize();
- }
- if (value is T typed)
- {
- return typed;
- }
- return (T)value;
- }
-
- private static long? ReadJsonInteger(object? value)
- {
- const double maxSafeInteger = 9007199254740991d;
- if (value is null)
- {
- return default;
- }
- double number;
- if (value is JsonElement json)
- {
- if (json.ValueKind == JsonValueKind.Null)
- {
- return default;
- }
- if (json.ValueKind != JsonValueKind.Number)
- {
- throw new JsonException("expected integer");
- }
- number = json.GetDouble();
- }
- else if (value is long longValue)
- {
- number = longValue;
- }
- else if (value is int intValue)
- {
- number = intValue;
- }
- else
- {
- throw new JsonException("expected integer");
- }
- if (double.IsNaN(number) || double.IsInfinity(number) || Math.Truncate(number) != number || Math.Abs(number) > maxSafeInteger)
- {
- throw new JsonException("expected integer");
- }
- return (long)number;
- }
-
- private static void RejectNull(string name, object? value)
- {
- if (value is null || value is JsonElement { ValueKind: JsonValueKind.Null })
- {
- throw new JsonException($"{name}: explicit null not allowed");
+ JsonRuntime.RejectNull("children", childrenValue);
+ _ = JsonRuntime.ReadJsonValue?>(childrenValue);
}
}
}
diff --git a/advanced/samples/dotnet/json_schema/api/showcase/Definitions.cs b/advanced/samples/dotnet/json_schema/api/showcase/Definitions.cs
new file mode 100644
index 00000000..c190d2dd
--- /dev/null
+++ b/advanced/samples/dotnet/json_schema/api/showcase/Definitions.cs
@@ -0,0 +1,288 @@
+//
+// Generated by nex-gen. DO NOT EDIT!
+#nullable enable
+#pragma warning disable CS1591
+
+using System.CodeDom.Compiler;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text.Json;
+
+namespace NexGen.ShowcaseService
+{
+
+ ///
+ /// A single constraint failure. is the JSON member path
+ /// (dotted for nested members); is a human-readable
+ /// message naming the bound and the offending value.
+ ///
+ [GeneratedCode("nex-gen", null)]
+ public sealed class Violation
+ {
+ public Violation(string path, string reason)
+ {
+ Path = path;
+ Reason = reason;
+ }
+
+ public string Path { get; }
+
+ public string Reason { get; }
+
+ ///
+ /// Returns "Path: Reason", or just Reason when the path is
+ /// empty.
+ ///
+ public override string ToString() =>
+ Path.Length == 0 ? Reason : Path + ": " + Reason;
+ }
+
+ ///
+ /// Aggregates every found while (de)serializing a
+ /// value, surfacing them all in one error rather than stopping at the first.
+ ///
+ [GeneratedCode("nex-gen", null)]
+ public sealed class ValidationException : JsonException
+ {
+ public ValidationException(IReadOnlyList violations)
+ : base(FormatMessage(violations))
+ {
+ Violations = violations;
+ }
+
+ ///
+ /// Every violation found, never a partial first-failure.
+ ///
+ public IReadOnlyList Violations { get; }
+
+ private static string FormatMessage(IReadOnlyList violations)
+ {
+ var parts = new string[violations.Count];
+ for (var index = 0; index < violations.Count; index++)
+ {
+ parts[index] = violations[index].ToString();
+ }
+ return $"{violations.Count} validation error(s): {string.Join("; ", parts)}";
+ }
+ }
+
+ ///
+ /// Read helpers shared by every generated model. Internal because they are an
+ /// implementation detail of the generated (de)serialization path rather than
+ /// part of the contract surface.
+ ///
+ [GeneratedCode("nex-gen", null)]
+ internal static class JsonRuntime
+ {
+ ///
+ /// The largest integer a JSON number carries losslessly (2^53-1).
+ ///
+ /// Exceeding it is a **contract violation**, reported through
+ /// with the offending member's path — not
+ /// a parse failure. Mirrors Go's `integerCap`.
+ ///
+ internal const long IntegerCap = 9007199254740991L;
+
+ ///
+ /// Reads an optional member out of the extension-data bag, falling back to
+ /// when absent.
+ ///
+ internal static T? ReadOptionalValue(
+ IDictionary members,
+ string name,
+ T? defaultValue = default)
+ {
+ if (!members.TryGetValue(name, out var value))
+ {
+ return defaultValue;
+ }
+ return ReadJsonValue(value);
+ }
+
+ internal static T? ReadJsonValue(object? value)
+ {
+ if (value is null)
+ {
+ return default;
+ }
+ if (typeof(T) == typeof(long?) || typeof(T) == typeof(long))
+ {
+ return (T?)(object?)ReadJsonInteger(value);
+ }
+ if (value is JsonElement json)
+ {
+ return json.Deserialize();
+ }
+ if (value is T typed)
+ {
+ return typed;
+ }
+ return (T)value;
+ }
+
+ ///
+ /// Reads a JSON number as an integer, rejecting non-integral values and
+ /// anything beyond the lossless integer range.
+ ///
+ internal static long? ReadJsonInteger(object? value)
+ {
+ if (value is null)
+ {
+ return default;
+ }
+ if (value is JsonElement json)
+ {
+ if (json.ValueKind == JsonValueKind.Null)
+ {
+ return default;
+ }
+ if (json.ValueKind != JsonValueKind.Number)
+ {
+ throw new JsonException("expected integer");
+ }
+ // Exact across the whole Int64 range, and fails for a non-integral
+ // number. Deliberately does not enforce IntegerCap: a value past
+ // 2^53-1 is a constraint violation the validator reports with a
+ // path, not a parse error. Reading through double would round it
+ // away before the validator ever saw it.
+ if (json.TryGetInt64(out var exact))
+ {
+ return exact;
+ }
+ // A number spelled with a decimal point but no fractional part —
+ // `1.0` — is a valid integer per JSON Schema, and TryGetInt64
+ // rejects that spelling. Fall back to the double reading, bounded
+ // to the range where double to long is exact. `% 1 != 0` also
+ // rejects NaN and infinity, whose remainder is NaN.
+ if (json.TryGetDouble(out var number)
+ && number % 1 == 0
+ && number >= -9007199254740992d
+ && number <= 9007199254740992d)
+ {
+ return (long)number;
+ }
+ throw new JsonException("expected integer");
+ }
+ if (value is long longValue)
+ {
+ return longValue;
+ }
+ if (value is int intValue)
+ {
+ return intValue;
+ }
+ throw new JsonException("expected integer");
+ }
+
+ ///
+ /// Reports every uniqueItems duplicate, each against the index where
+ /// the value was first seen.
+ ///
+ /// A repeated value therefore yields one violation per later occurrence
+ /// rather than one per pair, which is what the other targets do.
+ ///
+ internal static void CollectDuplicateItems(
+ IReadOnlyList items,
+ string path,
+ List violations)
+ where T : notnull
+ {
+ var seen = new Dictionary(items.Count);
+ for (var index = 0; index < items.Count; index++)
+ {
+ if (seen.TryGetValue(items[index], out var first))
+ {
+ violations.Add(new Violation(
+ path,
+ $"duplicate items: element at index {index} equals index {first}"));
+ }
+ else
+ {
+ seen[items[index]] = index;
+ }
+ }
+ }
+
+ ///
+ /// Counts elements equal to a contains const value, feeding the
+ /// minContains/maxContains occurrence window.
+ ///
+ internal static int CountMatchingItems(IReadOnlyList items, T expected)
+ {
+ var comparer = EqualityComparer.Default;
+ var count = 0;
+ foreach (var item in items)
+ {
+ if (comparer.Equals(item, expected))
+ {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ ///
+ /// Counts Unicode code points, which is the unit JSON Schema's
+ /// minLength/maxLength measure.
+ ///
+ /// string.Length counts UTF-16 code units, so it would score an
+ /// astral character such as U+1F600 as 2 and reject a value the contract
+ /// permits. This matches Go's utf8.RuneCountInString and Java's
+ /// codePointCount, including counting an unpaired surrogate as one.
+ ///
+ internal static int CodePointCount(string value)
+ {
+ var count = 0;
+ for (var index = 0; index < value.Length; index++)
+ {
+ count++;
+ if (char.IsHighSurrogate(value[index])
+ && index + 1 < value.Length
+ && char.IsLowSurrogate(value[index + 1]))
+ {
+ index++;
+ }
+ }
+ return count;
+ }
+
+ ///
+ /// Quotes a string for a violation reason, mirroring Go's %q for the
+ /// values a contract admits. Used by the enum reason, which names the
+ /// offending value alongside the admitted set.
+ ///
+ internal static string Quote(string value) => "\"" + value + "\"";
+
+ ///
+ /// Joins a violation path prefix to a member name, so a nested model
+ /// reports page.blocks.order rather than a bare order.
+ ///
+ internal static string JoinPath(string prefix, string name) =>
+ prefix.Length == 0 ? name : prefix + "." + name;
+
+ ///
+ /// Renders a number for a violation reason using the invariant culture, so
+ /// the message never picks up a locale's decimal separator and stays
+ /// byte-identical to the other targets' diagnostics.
+ ///
+ internal static string FormatNumber(double value) =>
+ value.ToString(CultureInfo.InvariantCulture);
+
+ ///
+ internal static string FormatNumber(long value) =>
+ value.ToString(CultureInfo.InvariantCulture);
+
+ ///
+ /// Rejects an explicit JSON null for a member the contract declares
+ /// non-nullable.
+ ///
+ internal static void RejectNull(string name, object? value)
+ {
+ if (value is null || value is JsonElement { ValueKind: JsonValueKind.Null })
+ {
+ throw new JsonException($"{name}: explicit null not allowed");
+ }
+ }
+ }
+
+}
diff --git a/advanced/samples/dotnet/json_schema/api/showcase/Models.cs b/advanced/samples/dotnet/json_schema/api/showcase/Models.cs
new file mode 100644
index 00000000..96471f14
--- /dev/null
+++ b/advanced/samples/dotnet/json_schema/api/showcase/Models.cs
@@ -0,0 +1,1674 @@
+//
+// Generated by nex-gen. DO NOT EDIT!
+#nullable enable
+#pragma warning disable CS1591
+
+using System;
+using System.CodeDom.Compiler;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.RegularExpressions;
+
+namespace NexGen.ShowcaseService
+{
+
+ ///
+ /// A nested object, open to forward-compatible extension.
+ ///
+ [GeneratedCode("nex-gen", null)]
+ public class Address : IJsonOnDeserialized
+ {
+ public Address(string street)
+ {
+ Street = street;
+ }
+
+ [JsonPropertyName("street")]
+ [JsonRequired]
+ public string Street { get; init; }
+ [JsonIgnore]
+ public string? City
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "city");
+ init
+ {
+ JsonRuntime.RejectNull("city", value);
+ AdditionalProperties["city"] = value;
+ }
+ }
+ [JsonIgnore]
+ public long? Zip
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "zip");
+ init
+ {
+ JsonRuntime.RejectNull("zip", value);
+ AdditionalProperties["zip"] = value;
+ }
+ }
+
+ [JsonExtensionData]
+ public Dictionary AdditionalProperties { get; set; } = new Dictionary();
+
+ ///
+ /// Validates every constraint the contract declares on this type, throwing a
+ /// single carrying all violations rather
+ /// than stopping at the first.
+ ///
+ public void Validate()
+ {
+ var violations = new List();
+ CollectViolations(violations, string.Empty);
+ if (violations.Count > 0)
+ {
+ throw new ValidationException(violations);
+ }
+ }
+
+ internal void CollectViolations(List violations, string path)
+ {
+ if (Zip is long zipValue)
+ {
+ if (zipValue < -JsonRuntime.IntegerCap || zipValue > JsonRuntime.IntegerCap)
+ {
+ violations.Add(new Violation(JsonRuntime.JoinPath(path, "zip"), "exceeds ±(2^53-1) integer cap"));
+ }
+ }
+ }
+
+ void IJsonOnDeserialized.OnDeserialized()
+ {
+ if (AdditionalProperties.TryGetValue("city", out var cityValue))
+ {
+ JsonRuntime.RejectNull("city", cityValue);
+ if (cityValue is JsonElement json3 && json3.ValueKind != JsonValueKind.String)
+ {
+ throw new JsonException($"{"city"}: expected string");
+ }
+ else if (cityValue is not JsonElement && cityValue is not string)
+ {
+ throw new JsonException($"{"city"}: expected string");
+ }
+ }
+ if (AdditionalProperties.TryGetValue("zip", out var zipValue))
+ {
+ JsonRuntime.RejectNull("zip", zipValue);
+ _ = JsonRuntime.ReadJsonValue(zipValue);
+ }
+ Validate();
+ }
+ }
+
+
+ ///
+ /// A string map with member-count and key-shape constraints: 1 to 3 entries, each key at most 8 code points (minProperties/maxProperties/propertyNames on a map-shaped object).
+ ///
+ [GeneratedCode("nex-gen", null)]
+ public class Attributes : IJsonOnDeserialized
+ {
+ [JsonExtensionData]
+ public Dictionary AdditionalProperties { get; set; } = new Dictionary();
+
+ ///
+ /// Validates every constraint the contract declares on this type, throwing a
+ /// single carrying all violations rather
+ /// than stopping at the first.
+ ///
+ public void Validate()
+ {
+ var violations = new List();
+ CollectViolations(violations, string.Empty);
+ if (violations.Count > 0)
+ {
+ throw new ValidationException(violations);
+ }
+ }
+
+ internal void CollectViolations(List violations, string path)
+ {
+ var propertyCount = AdditionalProperties.Count;
+ if (propertyCount < 1)
+ {
+ violations.Add(new Violation(path, "must have at least 1 properties, got " + propertyCount));
+ }
+ if (propertyCount > 3)
+ {
+ violations.Add(new Violation(path, "must have at most 3 properties, got " + propertyCount));
+ }
+ foreach (var propertyName in AdditionalProperties.Keys)
+ {
+ var nameLength = JsonRuntime.CodePointCount(propertyName);
+ if (nameLength > 8)
+ {
+ violations.Add(new Violation(JsonRuntime.JoinPath(path, propertyName), $"invalid property name \"{propertyName}\": must have length <= 8, got {nameLength}"));
+ }
+ }
+ }
+
+ void IJsonOnDeserialized.OnDeserialized()
+ {
+ foreach (var entry in AdditionalProperties)
+ {
+ if (entry.Value is JsonElement json3 && json3.ValueKind != JsonValueKind.String)
+ {
+ throw new JsonException($"{entry.Key}: expected string");
+ }
+ else if (entry.Value is not JsonElement && entry.Value is not string)
+ {
+ throw new JsonException($"{entry.Key}: expected string");
+ }
+ }
+ Validate();
+ }
+ }
+
+
+ ///
+ /// A circle branch of the Shape tagged union.
+ ///
+ [GeneratedCode("nex-gen", null)]
+ public class Circle : Shape
+ {
+ public Circle(double radius)
+ {
+ Radius = radius;
+ }
+
+ private string kindValue = "circle";
+
+ [JsonPropertyName("kind")]
+ [JsonRequired]
+ public string Kind
+ {
+ get => kindValue;
+ init
+ {
+ if (value != "circle")
+ {
+ throw new JsonException("kind must equal \"circle\"");
+ }
+ kindValue = value;
+ }
+ }
+ [JsonPropertyName("radius")]
+ [JsonRequired]
+ public double Radius { get; init; }
+
+ [JsonExtensionData]
+ public Dictionary AdditionalProperties { get; set; } = new Dictionary();
+
+ ///
+ /// Validates every constraint the contract declares on this type, throwing a
+ /// single carrying all violations rather
+ /// than stopping at the first.
+ ///
+ public override void Validate()
+ {
+ var violations = new List();
+ CollectViolations(violations, string.Empty);
+ if (violations.Count > 0)
+ {
+ throw new ValidationException(violations);
+ }
+ }
+
+ internal override void CollectViolations(List violations, string path)
+ {
+ }
+ }
+
+
+ ///
+ /// Contact details with a conditional requirement and a member-count bound: a shipping street requires a shipping zip (dependentRequired), and the object must carry 1 to 3 members (minProperties/maxProperties on a declared-property object). Also exercises the type-level `x-<lang>-name` override (the Stage 4 escape hatch): the emitted type is renamed to the derived name plus a per-language suffix (Go `ContactGo`, TS `ContactTs`, Python `ContactPy`, Java `ContactJava`) at its declaration and at every `$ref`, while the wire `$ref` name stays `Contact`.
+ ///
+ [GeneratedCode("nex-gen", null)]
+ public class Contact : IJsonOnDeserialized
+ {
+ [JsonIgnore]
+ public string? Email
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "email");
+ init
+ {
+ JsonRuntime.RejectNull("email", value);
+ AdditionalProperties["email"] = value;
+ }
+ }
+ [JsonIgnore]
+ public string? ShippingStreet
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "shippingStreet");
+ init
+ {
+ JsonRuntime.RejectNull("shippingStreet", value);
+ AdditionalProperties["shippingStreet"] = value;
+ }
+ }
+ [JsonIgnore]
+ public string? ShippingZip
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "shippingZip");
+ init
+ {
+ JsonRuntime.RejectNull("shippingZip", value);
+ AdditionalProperties["shippingZip"] = value;
+ }
+ }
+
+ [JsonExtensionData]
+ public Dictionary AdditionalProperties { get; set; } = new Dictionary();
+
+ ///
+ /// Validates every constraint the contract declares on this type, throwing a
+ /// single carrying all violations rather
+ /// than stopping at the first.
+ ///
+ public void Validate()
+ {
+ var violations = new List();
+ CollectViolations(violations, string.Empty);
+ if (violations.Count > 0)
+ {
+ throw new ValidationException(violations);
+ }
+ }
+
+ internal void CollectViolations(List violations, string path)
+ {
+ var propertyCount = AdditionalProperties.Count;
+ if (propertyCount < 1)
+ {
+ violations.Add(new Violation(path, "must have at least 1 properties, got " + propertyCount));
+ }
+ if (propertyCount > 3)
+ {
+ violations.Add(new Violation(path, "must have at most 3 properties, got " + propertyCount));
+ }
+ if (AdditionalProperties.ContainsKey("shippingStreet") && !AdditionalProperties.ContainsKey("shippingZip"))
+ {
+ violations.Add(new Violation(JsonRuntime.JoinPath(path, "shippingZip"), "property \"shippingZip\" is required when \"shippingStreet\" is present"));
+ }
+ }
+
+ void IJsonOnDeserialized.OnDeserialized()
+ {
+ if (AdditionalProperties.TryGetValue("email", out var emailValue))
+ {
+ JsonRuntime.RejectNull("email", emailValue);
+ if (emailValue is JsonElement json3 && json3.ValueKind != JsonValueKind.String)
+ {
+ throw new JsonException($"{"email"}: expected string");
+ }
+ else if (emailValue is not JsonElement && emailValue is not string)
+ {
+ throw new JsonException($"{"email"}: expected string");
+ }
+ }
+ if (AdditionalProperties.TryGetValue("shippingStreet", out var shippingStreetValue))
+ {
+ JsonRuntime.RejectNull("shippingStreet", shippingStreetValue);
+ if (shippingStreetValue is JsonElement json3 && json3.ValueKind != JsonValueKind.String)
+ {
+ throw new JsonException($"{"shippingStreet"}: expected string");
+ }
+ else if (shippingStreetValue is not JsonElement && shippingStreetValue is not string)
+ {
+ throw new JsonException($"{"shippingStreet"}: expected string");
+ }
+ }
+ if (AdditionalProperties.TryGetValue("shippingZip", out var shippingZipValue))
+ {
+ JsonRuntime.RejectNull("shippingZip", shippingZipValue);
+ if (shippingZipValue is JsonElement json3 && json3.ValueKind != JsonValueKind.String)
+ {
+ throw new JsonException($"{"shippingZip"}: expected string");
+ }
+ else if (shippingZipValue is not JsonElement && shippingZipValue is not string)
+ {
+ throw new JsonException($"{"shippingZip"}: expected string");
+ }
+ }
+ Validate();
+ }
+ }
+
+
+ ///
+ /// Arbitrary string key/value labels (typed map).
+ ///
+ [GeneratedCode("nex-gen", null)]
+ public class Labels : IJsonOnDeserialized
+ {
+ [JsonExtensionData]
+ public Dictionary AdditionalProperties { get; set; } = new Dictionary();
+
+ ///
+ /// Validates every constraint the contract declares on this type, throwing a
+ /// single carrying all violations rather
+ /// than stopping at the first.
+ ///
+ public void Validate()
+ {
+ var violations = new List();
+ CollectViolations(violations, string.Empty);
+ if (violations.Count > 0)
+ {
+ throw new ValidationException(violations);
+ }
+ }
+
+ internal void CollectViolations(List violations, string path)
+ {
+ var propertyCount = AdditionalProperties.Count;
+ if (propertyCount > 50)
+ {
+ violations.Add(new Violation(path, "must have at most 50 properties, got " + propertyCount));
+ }
+ }
+
+ void IJsonOnDeserialized.OnDeserialized()
+ {
+ foreach (var entry in AdditionalProperties)
+ {
+ if (entry.Value is JsonElement json3 && json3.ValueKind != JsonValueKind.String)
+ {
+ throw new JsonException($"{entry.Key}: expected string");
+ }
+ else if (entry.Value is not JsonElement && entry.Value is not string)
+ {
+ throw new JsonException($"{entry.Key}: expected string");
+ }
+ }
+ Validate();
+ }
+ }
+
+
+ ///
+ /// A closed object; unknown members are rejected.
+ ///
+ [GeneratedCode("nex-gen", null)]
+ public class Settings : IJsonOnDeserialized
+ {
+ [JsonIgnore]
+ public string? Theme
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "theme");
+ init
+ {
+ JsonRuntime.RejectNull("theme", value);
+ AdditionalProperties["theme"] = value;
+ }
+ }
+ [JsonIgnore]
+ public long? FontSize
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "fontSize");
+ init
+ {
+ JsonRuntime.RejectNull("fontSize", value);
+ AdditionalProperties["fontSize"] = value;
+ }
+ }
+
+ [JsonExtensionData]
+ public Dictionary AdditionalProperties { get; set; } = new Dictionary();
+
+ ///
+ /// Validates every constraint the contract declares on this type, throwing a
+ /// single carrying all violations rather
+ /// than stopping at the first.
+ ///
+ public void Validate()
+ {
+ var violations = new List();
+ CollectViolations(violations, string.Empty);
+ if (violations.Count > 0)
+ {
+ throw new ValidationException(violations);
+ }
+ }
+
+ internal void CollectViolations(List violations, string path)
+ {
+ if (FontSize is long fontSizeValue)
+ {
+ if (fontSizeValue < -JsonRuntime.IntegerCap || fontSizeValue > JsonRuntime.IntegerCap)
+ {
+ violations.Add(new Violation(JsonRuntime.JoinPath(path, "fontSize"), "exceeds ±(2^53-1) integer cap"));
+ }
+ }
+ }
+
+ void IJsonOnDeserialized.OnDeserialized()
+ {
+ foreach (var key in AdditionalProperties.Keys)
+ {
+ if (key != "theme" && key != "fontSize")
+ {
+ throw new JsonException($"Unknown field `{key}`.");
+ }
+ }
+ if (AdditionalProperties.TryGetValue("theme", out var themeValue))
+ {
+ JsonRuntime.RejectNull("theme", themeValue);
+ if (themeValue is JsonElement json3 && json3.ValueKind != JsonValueKind.String)
+ {
+ throw new JsonException($"{"theme"}: expected string");
+ }
+ else if (themeValue is not JsonElement && themeValue is not string)
+ {
+ throw new JsonException($"{"theme"}: expected string");
+ }
+ }
+ if (AdditionalProperties.TryGetValue("fontSize", out var fontSizeValue))
+ {
+ JsonRuntime.RejectNull("fontSize", fontSizeValue);
+ _ = JsonRuntime.ReadJsonValue(fontSizeValue);
+ }
+ Validate();
+ }
+ }
+
+
+ ///
+ /// A closed sum type (discriminated union) of Circle | Square, tagged by the shared required `kind` const. Selection reads `kind` and routes to the matching branch; an unknown tag is a Violation.
+ ///
+ [JsonConverter(typeof(ShapeJsonConverter))]
+ [GeneratedCode("nex-gen", null)]
+ public abstract class Shape
+ {
+ private protected Shape()
+ {
+ }
+
+ ///
+ /// Validates the selected branch, throwing a single
+ /// carrying every violation.
+ ///
+ public abstract void Validate();
+
+ internal abstract void CollectViolations(List violations, string path);
+ }
+
+ [GeneratedCode("nex-gen", null)]
+ internal sealed class ShapeJsonConverter : JsonConverter
+ {
+ public override Shape Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ using var document = JsonDocument.ParseValue(ref reader);
+ var root = document.RootElement;
+ if (root.ValueKind != JsonValueKind.Object)
+ {
+ throw new ValidationException(new List
+ {
+ new Violation(string.Empty, "expected one of: Circle, Square"),
+ });
+ }
+ if (!root.TryGetProperty("kind", out var tag))
+ {
+ throw new ValidationException(new List
+ {
+ new Violation(string.Empty, "discriminator \"kind\" is required"),
+ });
+ }
+ var raw = root.GetRawText();
+ switch (tag.ValueKind == JsonValueKind.String ? tag.GetString() : null)
+ {
+ case "circle":
+ return JsonSerializer.Deserialize(raw, options)!;
+ case "square":
+ return JsonSerializer.Deserialize(raw, options)!;
+ default:
+ throw new ValidationException(new List
+ {
+ new Violation(string.Empty, $"unknown discriminator kind {tag.GetRawText()}: expected one of [\"circle\", \"square\"]"),
+ });
+ }
+ }
+
+ public override void Write(Utf8JsonWriter writer, Shape value, JsonSerializerOptions options)
+ {
+ JsonSerializer.Serialize(writer, value, value.GetType(), options);
+ }
+ }
+
+
+ ///
+ /// Root object exercising the supported JSON-Schema feature subset: required and optional fields of every scalar type, optional+nullable and required+nullable members, arrays, a nested object via $ref, a typed-map, a closed object, an open (catch-all) object, a string const, a scalar default, and member docs.
+ ///
+ [GeneratedCode("nex-gen", null)]
+ public class Showcase : IJsonOnDeserialized
+ {
+ public Showcase(string status, long tier, double scale, string name, long count, bool active, string? category)
+ {
+ Status = status;
+ Tier = tier;
+ Scale = scale;
+ Name = name;
+ Count = count;
+ Active = active;
+ Category = category;
+ }
+
+ ///
+ /// Discriminator; always "showcase".
+ ///
+ private string kindValue = "showcase";
+
+ [JsonPropertyName("kind")]
+ [JsonRequired]
+ public string Kind
+ {
+ get => kindValue;
+ init
+ {
+ if (value != "showcase")
+ {
+ throw new JsonException("kind must equal \"showcase\"");
+ }
+ kindValue = value;
+ }
+ }
+ ///
+ /// Integer const; always 1. Also exercises the single-`const` value override: `x-go-const-name`/`x-java-const-name` rename the emitted constant to the derived name plus a per-language suffix (Go `RevisionGo`, Java `REVISION_JAVA`) while the wire value stays `1`. TS/Python are inert here (no const override keyword — the value is emitted as a plain literal type).
+ ///
+ private long revisionValue = 1;
+
+ [JsonPropertyName("revision")]
+ [JsonRequired]
+ public long Revision
+ {
+ get => revisionValue;
+ init
+ {
+ if (value != 1)
+ {
+ throw new JsonException("revision must equal 1");
+ }
+ revisionValue = value;
+ }
+ }
+ ///
+ /// Boolean const; always true.
+ ///
+ private bool enabledValue = true;
+
+ [JsonPropertyName("enabled")]
+ [JsonRequired]
+ public bool Enabled
+ {
+ get => enabledValue;
+ init
+ {
+ if (value != true)
+ {
+ throw new JsonException("enabled must equal true");
+ }
+ enabledValue = value;
+ }
+ }
+ ///
+ /// Closed string value set. Also exercises the enum value-constant override: `x-go-enum-names`/`x-java-enum-names` rename the `active` value's emitted constant to the value name plus a per-language suffix (Go `ActiveGo`, Java `ACTIVE_JAVA`) while the wire value stays `active`. TS/Python are inert here (no enum override keyword).
+ ///
+ [JsonPropertyName("status")]
+ [JsonRequired]
+ public string Status { get; init; }
+ ///
+ /// Closed integer value set.
+ ///
+ [JsonPropertyName("tier")]
+ [JsonRequired]
+ public long Tier { get; init; }
+ ///
+ /// Closed number value set (exercises the Python float exception: emitted as plain float, validated by membership).
+ ///
+ [JsonPropertyName("scale")]
+ [JsonRequired]
+ public double Scale { get; init; }
+ ///
+ /// Required human-readable name, 1 to 64 code points.
+ ///
+ [JsonPropertyName("name")]
+ [JsonRequired]
+ public string Name { get; init; }
+ ///
+ /// Required integer scalar.
+ ///
+ [JsonPropertyName("count")]
+ [JsonRequired]
+ public long Count { get; init; }
+ ///
+ /// Required boolean scalar.
+ ///
+ [JsonPropertyName("active")]
+ [JsonRequired]
+ public bool Active { get; init; }
+ ///
+ /// Optional short name, at most 12 code points.
+ ///
+ [JsonIgnore]
+ public string? Nickname
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "nickname");
+ init
+ {
+ JsonRuntime.RejectNull("nickname", value);
+ AdditionalProperties["nickname"] = value;
+ }
+ }
+ ///
+ /// Optional code, 2 to 5 code points. Counted in Unicode code points, so a multi-byte value (e.g. "a😀b", 3 code points / 6 UTF-8 bytes) is valid.
+ ///
+ [JsonIgnore]
+ public string? Code
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "code");
+ init
+ {
+ JsonRuntime.RejectNull("code", value);
+ AdditionalProperties["code"] = value;
+ }
+ }
+ ///
+ /// Optional product code: 2 to 4 uppercase ASCII letters, anchored (`^[A-Z]{2,4}$`). Exercises the RE2-safe `pattern` gate.
+ ///
+ [JsonIgnore]
+ public string? Sku
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "sku");
+ init
+ {
+ JsonRuntime.RejectNull("sku", value);
+ AdditionalProperties["sku"] = value;
+ }
+ }
+ ///
+ /// Optional two-word phrase separated by whitespace (`^\S+\s\S+$`). Exercises the loader's `\s`/`\S` → ASCII-class normalization and the per-target `$` end-anchor rewrite (Python `\Z` / Java `\z`), so a Unicode space (NBSP) and a trailing newline are rejected consistently across all four languages.
+ ///
+ [JsonIgnore]
+ public string? Phrase
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "phrase");
+ init
+ {
+ JsonRuntime.RejectNull("phrase", value);
+ AdditionalProperties["phrase"] = value;
+ }
+ }
+ ///
+ /// Optional request identifier; asserted RFC 4122 UUID via `format: uuid`. Stays `string`-typed (format assertion, no materialization); the pinned regex is validated identically across all four languages.
+ ///
+ [JsonIgnore]
+ public string? RequestId
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "requestId");
+ init
+ {
+ JsonRuntime.RejectNull("requestId", value);
+ AdditionalProperties["requestId"] = value;
+ }
+ }
+ ///
+ /// Optional contact address; asserted ASCII dot-atom `format: email` (single `@`, >=2-label domain, total length <= 254, guard-before-regex).
+ ///
+ [JsonIgnore]
+ public string? ContactEmail
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "contactEmail");
+ init
+ {
+ JsonRuntime.RejectNull("contactEmail", value);
+ AdditionalProperties["contactEmail"] = value;
+ }
+ }
+ ///
+ /// Optional host name; asserted RFC 1123 `format: hostname` (LDH labels, total length <= 253).
+ ///
+ [JsonIgnore]
+ public string? Host
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "host");
+ init
+ {
+ JsonRuntime.RejectNull("host", value);
+ AdditionalProperties["host"] = value;
+ }
+ }
+ ///
+ /// Optional homepage; asserted RFC 3986 `format: uri` (scheme required, ASCII only; an IP-literal host is validated by the spliced ipv6 grammar).
+ ///
+ [JsonIgnore]
+ public string? Homepage
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "homepage");
+ init
+ {
+ JsonRuntime.RejectNull("homepage", value);
+ AdditionalProperties["homepage"] = value;
+ }
+ }
+ ///
+ /// Optional gateway address; asserted dotted-quad IPv4 via format ipv4.
+ ///
+ [JsonIgnore]
+ public string? Gateway
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "gateway");
+ init
+ {
+ JsonRuntime.RejectNull("gateway", value);
+ AdditionalProperties["gateway"] = value;
+ }
+ }
+ ///
+ /// Optional binary payload carried as a `contentEncoding: base64` string, materialized to native bytes (Go []byte, TS Uint8Array, Python bytes, Java byte[]). The wire is canonical padded standard base64; a malformed value is rejected by the pinned regex before decode.
+ ///
+ [JsonIgnore]
+ public string? Blob
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "blob");
+ init
+ {
+ JsonRuntime.RejectNull("blob", value);
+ AdditionalProperties["blob"] = value;
+ }
+ }
+ ///
+ /// Optional binary payload carried as a `contentEncoding: base64url` string (URL-safe alphabet, unpadded, RFC 4648 §5), materialized to the same native bytes type. The same bytes encode to a different wire than base64 ("Pj4+" vs "Pj4-").
+ ///
+ [JsonIgnore]
+ public string? UrlBlob
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "urlBlob");
+ init
+ {
+ JsonRuntime.RejectNull("urlBlob", value);
+ AdditionalProperties["urlBlob"] = value;
+ }
+ }
+ ///
+ /// Optional integer with a schema default.
+ ///
+ [JsonIgnore]
+ public long? Retries
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "retries", 3);
+ init
+ {
+ JsonRuntime.RejectNull("retries", value);
+ AdditionalProperties["retries"] = value;
+ }
+ }
+ [JsonIgnore]
+ public bool? Verbose
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "verbose");
+ init
+ {
+ JsonRuntime.RejectNull("verbose", value);
+ AdditionalProperties["verbose"] = value;
+ }
+ }
+ ///
+ /// Optional string with a schema default, surfaced on read.
+ ///
+ [JsonIgnore]
+ public string? Greeting
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "greeting", "hello");
+ init
+ {
+ JsonRuntime.RejectNull("greeting", value);
+ AdditionalProperties["greeting"] = value;
+ }
+ }
+ ///
+ /// Optional boolean with a schema default.
+ ///
+ [JsonIgnore]
+ public bool? Debug
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "debug", false);
+ init
+ {
+ JsonRuntime.RejectNull("debug", value);
+ AdditionalProperties["debug"] = value;
+ }
+ }
+ ///
+ /// Deprecated legacy identifier; prefer `requestId`. Exercises the native deprecation marker (Go // Deprecated:, TS @deprecated, Java @Deprecated, Python PEP 702 @deprecated). Also exercises the property-level `x-<lang>-name` override (the Stage 4 escape hatch): the emitted member identifier is renamed to the derived name plus a per-language suffix (Go `LegacyIdGo`, TS `legacyIdTs`, Python `legacy_id_py`, Java `legacyIdJava`) while the wire name stays `legacyId` (json tag / alias / @JsonProperty).
+ ///
+ [JsonIgnore]
+ public string? LegacyId
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "legacyId");
+ init
+ {
+ JsonRuntime.RejectNull("legacyId", value);
+ AdditionalProperties["legacyId"] = value;
+ }
+ }
+ ///
+ /// Optional and nullable; may be absent or explicitly null.
+ ///
+ [JsonIgnore]
+ public string? MiddleName
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "middleName");
+ init
+ {
+ AdditionalProperties["middleName"] = value;
+ }
+ }
+ ///
+ /// Required but nullable; may be explicitly cleared to null.
+ ///
+ [JsonPropertyName("category")]
+ [JsonRequired]
+ public string? Category { get; init; }
+ ///
+ /// Optional integer bounded to the inclusive range [1, 10].
+ ///
+ [JsonIgnore]
+ public long? Priority
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "priority");
+ init
+ {
+ JsonRuntime.RejectNull("priority", value);
+ AdditionalProperties["priority"] = value;
+ }
+ }
+ ///
+ /// Optional integer that must be strictly greater than 0.
+ ///
+ [JsonIgnore]
+ public long? Level
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "level");
+ init
+ {
+ JsonRuntime.RejectNull("level", value);
+ AdditionalProperties["level"] = value;
+ }
+ }
+ ///
+ /// Optional number that must be a non-negative multiple of 5.
+ ///
+ [JsonIgnore]
+ public double? Ratio
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "ratio");
+ init
+ {
+ JsonRuntime.RejectNull("ratio", value);
+ AdditionalProperties["ratio"] = value;
+ }
+ }
+ ///
+ /// Optional integer that must be a multiple of 3.
+ ///
+ [JsonIgnore]
+ public long? Step
+ {
+ get => JsonRuntime.ReadOptionalValue(AdditionalProperties, "step");
+ init
+ {
+ JsonRuntime.RejectNull("step", value);
+ AdditionalProperties["step"] = value;
+ }
+ }
+ ///
+ /// Ordered list of free-form tags; 1 to 5 entries.
+ ///
+ [JsonIgnore]
+ public IReadOnlyList? Tags
+ {
+ get => JsonRuntime.ReadOptionalValue?>(AdditionalProperties, "tags");
+ init
+ {
+ JsonRuntime.RejectNull("tags", value);
+ AdditionalProperties["tags"] = value;
+ }
+ }
+ ///
+ /// Alternate names; each must be distinct.
+ ///
+ [JsonIgnore]
+ public IReadOnlyList? Aliases
+ {
+ get => JsonRuntime.ReadOptionalValue?>(AdditionalProperties, "aliases");
+ init
+ {
+ JsonRuntime.RejectNull("aliases", value);
+ AdditionalProperties["aliases"] = value;
+ }
+ }
+ ///
+ /// Access roles; must contain between one and two "admin" entries.
+ ///
+ [JsonIgnore]
+ public IReadOnlyList? Roles
+ {
+ get => JsonRuntime.ReadOptionalValue?>(AdditionalProperties, "roles");
+ init
+ {
+ JsonRuntime.RejectNull("roles", value);
+ AdditionalProperties["roles"] = value;
+ }
+ }
+ ///
+ /// Disjoint-kind union (oneOf sum type): the wire value is either a string or an integer, selected by its JSON token. Not a member of a discriminated union — the token itself is the selector.
+ ///
+ [JsonIgnore]
+ public object? IdOrName
+ {
+ get => JsonRuntime.ReadOptionalValue