using System.Text.Json; namespace StellaOps.Canonical.Json; public static partial class CanonJson { /// /// Canonicalizes an object with version marker for content-addressed hashing. /// The version marker is embedded as the first field in the canonical JSON, /// ensuring stable hashes even if canonicalization logic evolves. /// /// The type to serialize. /// The object to canonicalize. /// Canonicalization version (default: Current). /// UTF-8 encoded canonical JSON bytes with version marker. public static byte[] CanonicalizeVersioned(T obj, string version = CanonVersion.Current) { ArgumentException.ThrowIfNullOrWhiteSpace(version); var json = JsonSerializer.SerializeToUtf8Bytes(obj, _defaultSerializerOptions); using var doc = JsonDocument.Parse(json); using var ms = new MemoryStream(); using var writer = new Utf8JsonWriter(ms, _defaultWriterOptions); WriteElementVersioned(doc.RootElement, writer, version); writer.Flush(); return ms.ToArray(); } /// /// Canonicalizes an object with version marker using custom serializer options. /// /// The type to serialize. /// The object to canonicalize. /// JSON serializer options to use for initial serialization. /// Canonicalization version (default: Current). /// UTF-8 encoded canonical JSON bytes with version marker. public static byte[] CanonicalizeVersioned(T obj, JsonSerializerOptions options, string version = CanonVersion.Current) { ArgumentException.ThrowIfNullOrWhiteSpace(version); var json = JsonSerializer.SerializeToUtf8Bytes(obj, options); using var doc = JsonDocument.Parse(json); using var ms = new MemoryStream(); using var writer = new Utf8JsonWriter(ms, CreateWriterOptions(options)); WriteElementVersioned(doc.RootElement, writer, version); writer.Flush(); return ms.ToArray(); } private static void WriteElementVersioned(JsonElement el, Utf8JsonWriter w, string version) { if (el.ValueKind == JsonValueKind.Object) { w.WriteStartObject(); // Write version marker first (underscore prefix ensures lexicographic first position) w.WriteString(CanonVersion.VersionFieldName, version); // Write remaining properties sorted foreach (var prop in el.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal)) { if (string.Equals(prop.Name, CanonVersion.VersionFieldName, StringComparison.Ordinal)) { continue; } w.WritePropertyName(prop.Name); WriteElementSorted(prop.Value, w); } w.WriteEndObject(); } else { // Non-object root: wrap in object with version marker w.WriteStartObject(); w.WriteString(CanonVersion.VersionFieldName, version); w.WritePropertyName("_value"); WriteElementSorted(el, w); w.WriteEndObject(); } } }