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();
}
}
}