Overview
Savvyio.Extensions.Text.Json adapts Savvy I/O contracts to System.Text.Json. It extends Savvyio.Domain and Savvyio.Messaging with a stream-based marshaller and converters that round-trip IRequest, IMetadataDictionary, IMessage<T>, signed messages, cloud events, SingleValueObject<T>, and ISO8601 date values.
The package is intentionally focused. It does not add dependency injection registration or a second serialization model. Instead, JsonMarshaller wires Savvy I/O-specific converters into the built-in JSON formatter defaults, while the converter extension methods let you register or remove the same behavior on explicit converter collections and JsonSerializerOptions instances.
Key APIs
JsonMarshaller is the package's main entry point for stream-based serialization. It exposes generic and type-based Serialize and Deserialize overloads, a Create factory, and a Default instance, and its static initialization path registers request, metadata, message, single-value-object, DateTime, and DateTimeOffset converters through JsonFormatterOptions.DefaultConverters.
JsonConverterExtensions.AddRequestConverter registers RequestConverter for IRequest implementations. During deserialization it creates an uninitialized instance, then fills writable properties or matching backing fields, and it throws NotSupportedException when a non-writable property cannot be matched to a supported backing-field pattern.
JsonConverterExtensions.AddMessageConverter registers MessageConverter for IMessage<T> and derived shapes. Source and tests show that it recreates plain messages, signed messages, CloudEvents, signed CloudEvents, and dictionary-backed CloudEvent extension attributes while preserving envelope fields such as id, source, type, time, and data.
JsonConverterExtensions.AddMetadataDictionaryConverter registers MetadataDictionaryConverter for IMetadataDictionary. The converter reads JSON objects into MetadataDictionary instances, and tests cover booleans, nulls, 64-bit integers, decimals, nested objects, arrays, and ISO8601 timestamp strings that materialize as DateTime values.
JsonConverterExtensions.AddSingleValueObjectConverter registers SingleValueObjectConverter for SingleValueObject<T>. It writes only the wrapped Value and recreates the domain type by invoking the target type's single-argument constructor during deserialization.
JsonConverterExtensions.AddDateTimeConverter and JsonConverterExtensions.AddDateTimeOffsetConverter register explicit round-trip date converters. Both writers emit the "O" format, and the tests verify that the converters can read the corresponding ISO8601 JSON strings back into .NET date values.
JsonSerializerOptionsExtensions.Clone copies an existing JsonSerializerOptions instance and optionally applies a setup callback to the clone. The package uses this together with JsonConverterExtensions.RemoveAllOf to avoid recursive converter re-entry when nested serialization needs to temporarily exclude the active IRequest or IMetadataDictionary converter.
Basic usage
using System;
using System.IO;
using Codebelt.Extensions.Xunit;
using Savvyio.Domain;
using Savvyio.Extensions.Text.Json;
using Xunit;
namespace MyProject.Tests;
public class JsonMarshallerTests : Test
{
public JsonMarshallerTests(ITestOutputHelper output) : base(output) { }
[Fact]
public void Serialize_SingleValueObject_RoundTripsProperty()
{
var marshaller = new JsonMarshaller();
var expected = new InventoryCheckpoint(
new LocationId(Guid.Parse("11111111-1111-1111-1111-111111111111")),
DateTime.SpecifyKind(new DateTime(2024, 11, 16, 23, 24, 17), DateTimeKind.Utc));
using var textStream = marshaller.Serialize(expected);
using var reader = new StreamReader(textStream);
var json = reader.ReadToEnd();
using var dataStream = marshaller.Serialize(expected);
var actual = marshaller.Deserialize<InventoryCheckpoint>(dataStream);
TestOutput.WriteLine(json);
Assert.DoesNotContain("\"value\"", json);
Assert.Equal(expected, actual);
}
private sealed record LocationId : SingleValueObject<Guid>
{
public LocationId(Guid value) : base(value) { }
}
private sealed record InventoryCheckpoint(LocationId LocationId, DateTime CapturedAt);
}
Use this pattern when a domain model contains SingleValueObject<T> members and you want the built-in JSON stack to round-trip them through JsonMarshaller without writing a custom converter per type.
It matters because the package preserves the domain type on deserialization while keeping the JSON payload scalar for the wrapped value instead of expanding it to an implementation-shaped object.
Installation
dotnet add package Savvyio.Extensions.Text.Json
Usage guidance
Use this package when a System.Text.Json boundary needs to carry Savvy I/O requests, messages, metadata dictionaries, or single-value domain types without hand-written converters for each contract. If your application wants container registration instead of direct marshaller or converter usage, prefer Savvyio.Extensions.DependencyInjection.Text.Json, and if you only serialize ordinary CLR models, plain System.Text.Json is the simpler choice.
Family packages
- 🏭Savvyio.App
- 📦Savvyio.Commands
- 📦Savvyio.Commands.Messaging
- 📦Savvyio.Core
- 📦Savvyio.Domain
- 📦Savvyio.Domain.EventSourcing
- 📦Savvyio.EventDriven
- 📦Savvyio.EventDriven.Messaging
- 📦Savvyio.Extensions.Dapper
- 📦Savvyio.Extensions.DapperExtensions
- 📦Savvyio.Extensions.DependencyInjection
- 📦Savvyio.Extensions.DependencyInjection.Dapper
- 📦Savvyio.Extensions.DependencyInjection.DapperExtensions
- 📦Savvyio.Extensions.DependencyInjection.Domain
- 📦Savvyio.Extensions.DependencyInjection.EFCore
- 📦Savvyio.Extensions.DependencyInjection.EFCore.Domain
- 📦Savvyio.Extensions.DependencyInjection.EFCore.Domain.EventSourcing
- 📦Savvyio.Extensions.DependencyInjection.NATS
- 📝Savvyio.Extensions.DependencyInjection.Newtonsoft.Json
- 📦Savvyio.Extensions.DependencyInjection.QueueStorage
- 📦Savvyio.Extensions.DependencyInjection.RabbitMQ
- 📦Savvyio.Extensions.DependencyInjection.SimpleQueueService
- 📝Savvyio.Extensions.DependencyInjection.Text.Json
- 📦Savvyio.Extensions.Dispatchers
- 📦Savvyio.Extensions.EFCore
- 📦Savvyio.Extensions.EFCore.Domain
- 📦Savvyio.Extensions.EFCore.Domain.EventSourcing
- 📦Savvyio.Extensions.NATS
- 📝Savvyio.Extensions.Newtonsoft.Json
- 📦Savvyio.Extensions.QueueStorage
- 📦Savvyio.Extensions.RabbitMQ
- 📦Savvyio.Extensions.SimpleQueueService
- 📦Savvyio.Messaging
- 📦Savvyio.Queries