Serialization
Centralize every JsonSerializerContext in your app behind a single AOT-safe ISerializer. Decorate a context with [ShinyJsonContext] and the source generator auto-registers it. Decorate an element type with [ShinyJsonInclude] and List<T>, T[], IEnumerable<T> and friends round-trip without reflection — even when the element carries an inline [JsonConverter].
| GitHub | |
| Downloads |
Features
Section titled “Features”- Single shared
ISerializerbacked by oneJsonSerializerOptionswhoseTypeInfoResolverChaincollects every contributed context Shiny.Jsonstatic accessor — self-bootstrapping, sibling toShiny.Stores. Works before DI exists (mobile cold-start)[ShinyJsonContext]source-generator marker on any user-declaredJsonSerializerContext→ emits a[ModuleInitializer]that auto-registers it. Noservices.AddJsonContext(...)calls needed[ShinyJsonInclude]source-generator marker on element types → AOT-safe collection wrappers (List<T>,T[],IEnumerable<T>,IReadOnlyList<T>,IList<T>,ICollection<T>,IAsyncEnumerable<T>) usingJsonMetadataServices.CreateListInfo<,>and friends- Composes with inline
[JsonConverter(typeof(MyConverter))]— the converter handles the element, the generated wrappers handle the collection - DI:
services.AddJsonSerialization()/AddJsonContext(...)/ConfigureJsonSerializer(...)/AddSerializer<T>()/UseSerializer() - Test isolation:
Shiny.Json.CreateTestScope(...)adds extras for the scope and resets the cached serializer on dispose
-
Install the NuGet package:
Terminal window dotnet add package Shiny.Extensions.Serialization -
Optionally register
ISerializerin DI (not required for the static accessor):builder.Services.AddJsonSerialization(); -
Decorate a normal
JsonSerializerContextpartial with[Shiny.ShinyJsonContext]. The Shiny source generator emits a[ModuleInitializer]callingShiny.Json.AddContext(MyAppJsonContext.Default)beforeMain:using System.Text.Json.Serialization;using Shiny;[ShinyJsonContext][JsonSerializable(typeof(MyDto))][JsonSerializable(typeof(MyOtherDto))]internal partial class MyAppJsonContext : JsonSerializerContext; -
Done.
Shiny.Json.Defaultand any DI-resolvedISerializerboth see your types. You do not needservices.AddJsonContext(...)anywhere.
Auto-Registering With [ShinyJsonContext]
Section titled “Auto-Registering With [ShinyJsonContext]”The recommended pattern. Decorate any normal STJ source-generator context and the Shiny generator emits a [ModuleInitializer] that calls Shiny.Json.AddContext(MyContext.Default) before any user code runs.
This is strictly better than services.AddJsonContext(MyContext.Default) for two reasons:
- No “forgot to register” bugs. Module initializers run regardless of which
AddX(...)extension the consumer called. (Real example: Shiny Locations hadAddGeofencingregisteringShinyLocationsJsonContext, butAddGpsdid not — a GPS-only app would have thrown at runtime under AOT. Switching to[ShinyJsonContext]removed the latent bug.) - Works before DI exists. Module init fires before
Main, so the staticShiny.Stores.Default(mobile cold-start) can use the serializer immediately.
Adding Collection Support With [ShinyJsonInclude]
Section titled “Adding Collection Support With [ShinyJsonInclude]”If an element type’s JsonTypeInfo is registered (anywhere in the chain) but List<T>/T[] throw “no metadata for type” under AOT, mark the element:
[ShinyJsonInclude]public partial class MyDto{ public string Name { get; set; } = "";}The generator emits an IJsonTypeInfoResolver providing AOT-safe wrappers for:
List<MyDto>MyDto[]IEnumerable<MyDto>IReadOnlyList<MyDto>/IReadOnlyCollection<MyDto>IList<MyDto>/ICollection<MyDto>IAsyncEnumerable<MyDto>
Each wrapper lazy-resolves the element JsonTypeInfo<MyDto> from the chain at runtime, so the element can come from any other registered context ([ShinyJsonContext]-decorated, hand-registered, third-party — doesn’t matter).
For types you don’t own, use the assembly form:
[assembly: Shiny.ShinyJsonInclude(typeof(SomeExternal.Vendor.Payload))]Composing With Inline JsonConverter<T>
Section titled “Composing With Inline JsonConverter<T>”The original motivating bug: a type carrying [JsonConverter(typeof(MyConverter))] serializes fine in isolation, but List<MyType> throws under AOT because STJ has no JsonTypeInfo<List<MyType>>. The two decorations compose:
[ShinyJsonInclude][JsonConverter(typeof(BoxedIntConverter))]public partial class BoxedInt{ public int Value { get; set; }}
public sealed class BoxedIntConverter : JsonConverter<BoxedInt>{ public override BoxedInt Read(ref Utf8JsonReader reader, Type t, JsonSerializerOptions o) => new() { Value = reader.GetInt32() };
public override void Write(Utf8JsonWriter writer, BoxedInt value, JsonSerializerOptions o) => writer.WriteNumberValue(value.Value);}BoxedInt serializes as a bare number via the inline converter. List<BoxedInt> goes through the Shiny-generated collection wrapper, which lazy-fetches the JsonTypeInfo<BoxedInt> that carries the inline converter. The result is [1, 2, 3] — under AOT, no reflection.
DI Patterns
Section titled “DI Patterns”public class MyService(ISerializer serializer){ public string Save(MyDto d) => serializer.Serialize(d); public MyDto Load(string j) => serializer.Deserialize<MyDto>(j);}
// Mutate options before first useservices.ConfigureJsonSerializer(o => o.WriteIndented = false);
// Hand-add a 3rd-party context you can't decorateservices.AddJsonContext(ThirdPartyJsonContext.Default);
// Swap the whole serializer (MessagePack, MemoryPack, etc.)services.AddSerializer<MyMessagePackSerializer>();host.Services.UseSerializer(); // snapshot DI-resolved instance into Shiny.Json.DefaultStatic Patterns
Section titled “Static Patterns”// Works anywhere — no DI requiredvar json = Shiny.Json.Default.Serialize(new MyDto { Name = "x" });var back = Shiny.Json.Default.Deserialize<MyDto>(json);
// Late additions (must be before first Serialize call)Shiny.Json.AddContext(SomeOtherContext.Default);Shiny.Json.Configure(o => o.WriteIndented = false);Diagnostics
Section titled “Diagnostics”| ID | Severity | Meaning |
|---|---|---|
SJSON002 | Error | [ShinyJsonInclude] applied to an unbound generic type. Use a closed constructed type. |
SJSON003 | Warning | [ShinyJsonInclude] was applied to type T, but no [JsonSerializable(typeof(T))] is declared on any JsonSerializerContext in this compilation. The generated collection wrappers will return null at runtime and serialization will throw. Add the [JsonSerializable] to a registered context, or suppress the warning if the element is registered in another assembly. |
Testing
Section titled “Testing”[Collection("ShinyJson")]public class MyTests{ [Fact] public void RoundTrip() { using var scope = Shiny.Json.CreateTestScope( extraResolvers: [ExtraContext.Default], extraConfigure: o => o.WriteIndented = false );
var json = Shiny.Json.Default.Serialize(new MyDto { Name = "x" }); Shiny.Json.Default.Deserialize<MyDto>(json).Name.ShouldBe("x"); }}Tests touching Shiny.Json must share an xUnit collection ([Collection("ShinyJson")]) because the registry is process-static.
AI Coding Assistant
Section titled “AI Coding Assistant”Step 1 — Add the marketplace:
claude plugin marketplace add shinyorg/skills Step 2 — Install the plugin:
claude plugin install shiny-extensions@shiny Step 1 — Add the marketplace:
copilot plugin marketplace add https://github.com/shinyorg/skills Step 2 — Install the plugin:
copilot plugin install shiny-extensions@shiny