MinimalSerializers.Json
1.0.5
dotnet add package MinimalSerializers.Json --version 1.0.5
NuGet\Install-Package MinimalSerializers.Json -Version 1.0.5
<PackageReference Include="MinimalSerializers.Json" Version="1.0.5" />
<PackageVersion Include="MinimalSerializers.Json" Version="1.0.5" />
<PackageReference Include="MinimalSerializers.Json" />
paket add MinimalSerializers.Json --version 1.0.5
#r "nuget: MinimalSerializers.Json, 1.0.5"
#:package MinimalSerializers.Json@1.0.5
#addin nuget:?package=MinimalSerializers.Json&version=1.0.5
#tool nuget:?package=MinimalSerializers.Json&version=1.0.5
MinimalSerializers.Json
Automatically generate System.Text.Json source-generation roots from [DataContract] graphs.
Stop hand-maintaining hundreds of [JsonSerializable(typeof(...))] attributes (and the T[] / List<T> variants ASP.NET needs). Mark your DTOs with [DataContract] / [DataMember], put [MinimalJsonSerializerContext] on a partial JsonSerializerContext, and let the package emit the roots before CoreCompile so the built-in STJ source generator produces serializers in a single dotnet build.
Why this exists
JsonSerializerContext is the right AOT-friendly path, but root registration is manual and brittle:
- every DTO
- every nested type
- every
List<T>/T[]/ dictionary shape used as a root
MinimalSerializers.Json fills the gap .NET should have shipped: discover the graph, emit the roots, reuse STJ.
How it works
[DataContract] models + partial context [MinimalJsonSerializerContext]
│
▼
MSBuild target (buildTransitive) → discovery task
│
▼
obj/.../*.MinimalJson.g.cs // [JsonSerializable(typeof(T))] + arrays/lists
│
▼
CoreCompile → System.Text.Json source generator
│
▼
Context.Default (same runtime path as hand-written contexts)
There is no generator ordering hack. Roslyn generators cannot see each other's main outputs, so this package writes compile-visible source via MSBuild before STJ runs.
Runtime performance is the same as a complete manual STJ context, because the serializers are still generated by STJ.
Install
dotnet add package MinimalSerializers.Json
No custom Target in your csproj. The package ships buildTransitive props/targets automatically.
Quick start
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
using MinimalSerializers.Json;
[DataContract]
public sealed record OrderDto
{
[DataMember] public required Guid Id { get; init; }
[DataMember] public required List<OrderItemDto> Items { get; init; }
}
[DataContract]
public sealed record OrderItemDto
{
[DataMember] public required string Sku { get; init; }
[DataMember] public required int Quantity { get; init; }
}
[MinimalJsonSerializerContext]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
public partial class AppJsonSerializerContext : JsonSerializerContext;
Wire into options / DI
using System.Text.Json;
using MinimalSerializers.Json;
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
options.AddMinimalJsonContext<AppJsonSerializerContext>();
// ASP.NET Core
builder.Services.ConfigureHttpJsonOptions(o =>
{
o.SerializerOptions.AddMinimalJsonContext<AppJsonSerializerContext>();
});
Or insert the context yourself (same as today):
options.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
What gets registered
For each discovered object/enum type T (from [DataContract] and nested [DataMember] graphs):
TT[](default on — important because ASP.NET often materializes arrays)List<T>(default on)
Also registers closed collection/dictionary shapes found on members when enabled.
TypeInfo property names
Collection and array roots are emitted with an explicit TypeInfoPropertyName so they cannot collide with DTOs named List*Dto (STJ SYSLIB1031):
[JsonSerializable(typeof(ListMoneyDetailsDto))]
[JsonSerializable(typeof(ListMoneyDetailsDto[]), TypeInfoPropertyName = "ArrayOf_ListMoneyDetailsDto")]
[JsonSerializable(typeof(List<MoneyDetailsDto>), TypeInfoPropertyName = "ListOf_MoneyDetailsDto")]
Plain object/enum roots keep STJ's default short names unless two types would still share the same short name (different namespaces), in which case Minimal assigns unique names. Prefer options.GetTypeInfo(typeof(T)) / JsonSerializer.Serialize(value, options) when you do not want to depend on generated property names; typed Context.Default.ListOf_… members follow the mangled names above.
Conventions
| Mark | Role |
|---|---|
[DataContract] |
Include type in discovery |
[DataMember] |
Include member in graph walk (if any DataMembers exist on the type) |
[IgnoreDataMember] / [JsonIgnore] |
Skip member |
[EnumMember] |
Preserved for STJ enum handling where applicable |
[MinimalJsonSerializerContext] |
Partial context to receive generated roots |
One context per project is the intended model (for example one DTO/project assembly → one context).
MSBuild properties
| Property | Default | Meaning |
|---|---|---|
MinimalJsonSerializerEnabled |
true |
Master switch |
MinimalJsonEmitArrays |
true |
Emit T[] roots |
MinimalJsonEmitList |
true |
Emit List<T> roots |
MinimalJsonEmitDeclaredCollections |
true |
Emit declared collection interface closed types |
MinimalJsonEmitDictionaries |
true |
Emit dictionary closed types |
MinimalJsonWarnOpenGenerics |
summary |
MSJ0004 mode: summary (one warning), all (per type), or none |
MinimalJson_EnableDesignTime |
true |
Generate during design-time builds |
Performance
The package does not invent a new serializer. It discovers your DataContract graph and emits the same [JsonSerializable] roots you would have written by hand, then the built-in System.Text.Json source generator builds the serializers.
So the fair comparison is three paths on the same payload:
| Path | What it is |
|---|---|
| Reflection | Stock STJ with DefaultJsonTypeInfoResolver — no JsonSerializerContext, no hand-written roots |
| Manual STJ | Complete hand-maintained [JsonSerializable(typeof(...))] context (the maintenance pain) |
| MinimalSerializers | Same STJ source-gen path as manual, but roots are discovered automatically |
Benchmarks use a warmed order with 64 line items and include both:
- options/resolver-chain APIs (
Serialize(value, options)/Deserialize<T>(json, options)) - typed source-gen APIs (
Serialize(value, Context.Default.T)/Deserialize(json, Context.Default.T))
Results
Representative numbers from benchmarks/MinimalSerializers.Json.Benchmarks on .NET 11 / Apple M4 Max (InProcess, warmed):
| Method | Mean | Allocated | vs reflection serialize |
|---|---|---|---|
| Reflection_Serialize (baseline) | 21.1 µs | 5.65 KB | 1.00× |
| ManualStj_Serialize | 9.4 µs | 5.34 KB | 0.55× |
| MinimalStj_Serialize | 11.0 µs | 5.34 KB | 0.64× |
| ManualStj_Serialize_TypeInfo | 3.5 µs | 5.34 KB | 0.20× |
| MinimalStj_Serialize_TypeInfo | 4.4 µs | 5.34 KB | 0.25× |
| Reflection_Deserialize | 11.2 µs | 11.2 KB | 0.65× |
| ManualStj_Deserialize | 15.1 µs | 18.4 KB | 0.88× |
| MinimalStj_Deserialize | 14.1 µs | 18.4 KB | 0.81× |
| ManualStj_Deserialize_TypeInfo | 15.7 µs | 18.4 KB | 0.91× |
| MinimalStj_Deserialize_TypeInfo | 14.5 µs | 18.4 KB | 0.84× |
Takeaways
- Minimal ≈ Manual on both serialize and deserialize (within noise). That is the point: you get the source-generated STJ path without typing every root and every
T[]/List<T>by hand. - Serialize is a clear win for source-gen. Prefer the typed API (
Context.Default.MyDto) when you can; it is roughly 4–6× faster than reflection here. Options-based source-gen is still about 2× faster than reflection. - Deserialize is not automatically faster than reflection on warm, property-based DTOs. Reflection’s cached metadata path can allocate less and finish sooner on this shape. Manual and Minimal stay locked together, so the gap is STJ source-gen vs reflection — not Minimal overhead.
- Prefer source-gen anyway for AOT/trimming, ASP.NET root coverage (
T/T[]/List<T>), and predictable startup. Minimal’s product win is authoring: one[MinimalJsonSerializerContext]partial instead of a brittle attribute list, at the same runtime speed as the hand-written context.
dotnet run --project benchmarks/MinimalSerializers.Json.Benchmarks -c Release
Samples
samples/Sample.Contracts— sample DataContract models + minimal contextsamples/Sample.Host— sample host that serializes/deserializes with options wiring
dotnet run --project samples/Sample.Host
Limitations (v1)
- Open generics are skipped as roots (closed constructed types only)
- Generic DTO inheritance of the form
Derived<T> : Base<T>(both DataContracts) can break the built-in STJ source generator with CS0102 (duplicate nested accessor types). MinimalSerializers emits MSJ0009 when it detects this shape. Prefer composition or flattened records. Closed constructions are still registered. - Polymorphism /
$typediscriminators are not auto-generated (keep your ownJsonTypeInfomodifiers) DataMember.Nameis not rewritten to[JsonPropertyName](STJ naming policies remain source of truth)- Multi-assembly discovery is not enabled by default (one project → one context)
Diagnostics
| Code | Meaning |
|---|---|
| MSJ0001 | No Minimal context / attribute missing |
| MSJ0002 | Context found but no DataContracts |
| MSJ0003 | Context not partial or not a JsonSerializerContext |
| MSJ0004 | Open generic skipped (summary / all / none via MinimalJsonWarnOpenGenerics) |
| MSJ0005 | Unresolvable member type skipped |
| MSJ0006 | Generation failure |
| MSJ0009 | Generic DataContract inheritance Derived<T> : Base<T> may break STJ source-gen (CS0102) |
Development
dotnet tool restore
dotnet build MinimalSerializers.slnx
dotnet test MinimalSerializers.slnx --settings coverage.runsettings
dotnet pack src/MinimalSerializers.Json/MinimalSerializers.Json.csproj -c Release -o artifacts/packages
Maintainer publish steps (Trusted Publishing / local push) live in PUBLISHING.md.
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. net11.0 is compatible. |
-
net10.0
- No dependencies.
-
net11.0
- No dependencies.
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
# Release Notes
## 1.0.5
- Invalidate `stamp.minimaljson` before the incremental skip when `*.MinimalJson.g.cs` is missing, so a leftover stamp cannot produce CS0534 (#7)
## 1.0.4
- MSJ0004 no longer warns for abstract open `[DataContract]` generics (they are still skipped as roots). Closed instantiations stay registered.
## 1.0.3
- Speed up package acceptance tests: pack once per class, disable parallelization (avoids CI hang from concurrent Tasks rebuilds)
- Includes 1.0.2 fixes: TypeInfoPropertyName for collection roots, MSJ0009 generic inheritance diagnostic, quieter MSJ0004
## 1.0.2
- Assign `TypeInfoPropertyName` for `List<T>` / array / collection roots to avoid SYSLIB1031 collisions with `List*Dto` types (#2)
- Detect generic DataContract inheritance `Derived<T> : Base<T>` and emit MSJ0009 guidance for the STJ CS0102 footgun (#3)
- Default MSJ0004 open-generic warnings to a single summary; configure with `MinimalJsonWarnOpenGenerics=summary|all|none` (#5)
- Package + unit regression tests for `List*Dto` collisions and generic DTO inheritance (#4)
## 1.0.1
- Ship package README and release notes on nuget.org
- README cleanup: generic samples/docs; publishing moved to PUBLISHING.md
## 1.0.0
- Initial release of MinimalSerializers.Json
- MSBuild pre-compile discovery of `[DataContract]` graphs
- Emits `[JsonSerializable]` roots including `T`, `T[]`, and `List<T>`
- `[MinimalJsonSerializerContext]` attribute and `AddMinimalJsonContext<T>()` helper
- Single-build guarantee with buildTransitive packaging
- Samples, tests, benchmarks, CI/release workflows