JSON Serialization (System.Text.Json)¶
The LayeredCraft.OptimizedEnums.SystemTextJson package adds source-generated, zero-reflection JsonConverter support for your OptimizedEnum types. Decorate a class with [OptimizedEnumJsonConverter] and the generator emits a concrete converter and wires it up via [JsonConverter] automatically — no factory, no runtime type-checking, full AOT compatibility.
Installation¶
Install the SystemTextJson package. The core LayeredCraft.OptimizedEnums package is pulled in automatically as a dependency — only one dotnet add is needed:
The Attribute¶
Two serialization strategies are available, controlled by the OptimizedEnumJsonConverterType enum:
| Strategy | Value | JSON representation | Deserialization input |
|---|---|---|---|
ByName |
0 |
"Pending" (the Name string) |
JSON string |
ByValue |
1 |
1 (the underlying Value) |
JSON number / string / bool depending on TValue |
Apply the attribute to your OptimizedEnum class:
using LayeredCraft.OptimizedEnums;
using LayeredCraft.OptimizedEnums.SystemTextJson;
[OptimizedEnumJsonConverter(OptimizedEnumJsonConverterType.ByName)]
public sealed partial class OrderStatus : OptimizedEnum<OrderStatus, int>
{
public static readonly OrderStatus Pending = new(1, nameof(Pending));
public static readonly OrderStatus Paid = new(2, nameof(Paid));
public static readonly OrderStatus Shipped = new(3, nameof(Shipped));
private OrderStatus(int value, string name) : base(value, name) { }
}
That is all the user code required. The generator handles everything else.
What Gets Generated¶
For the ByName example above, the generator emits two things into a single .g.cs file:
1. A partial class stub stamped with [JsonConverter]:
This is how System.Text.Json discovers the converter — the attribute is on the type itself, so no manual registration in JsonSerializerOptions is ever needed.
2. A concrete, non-generic converter:
[GeneratedCode(...)]
internal sealed class OrderStatusNameJsonConverter
: JsonConverter<global::MyApp.Domain.OrderStatus>
{
public override OrderStatus Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.String)
throw new JsonException(...);
var name = reader.GetString()!;
if (!OrderStatus.TryFromName(name, out var result))
throw new JsonException($"'{name}' is not a valid name for OrderStatus.");
return result!;
}
public override void Write(
Utf8JsonWriter writer,
OrderStatus value,
JsonSerializerOptions options)
=> writer.WriteStringValue(value.Name);
}
ByName Strategy¶
Serializes using the member's Name string. Suitable when your JSON needs to be human-readable or stable across value changes.
[OptimizedEnumJsonConverter(OptimizedEnumJsonConverterType.ByName)]
public sealed partial class OrderStatus : OptimizedEnum<OrderStatus, int> { ... }
Deserialization calls TryFromName with Ordinal string comparison (the same as the hand-written lookup tables). An unrecognised name throws JsonException.
ByValue Strategy¶
Serializes using the member's Value. Suitable for compact payloads or when matching external integer/string codes.
[OptimizedEnumJsonConverter(OptimizedEnumJsonConverterType.ByValue)]
public sealed partial class OrderStatus : OptimizedEnum<OrderStatus, int> { ... }
Deserialization delegates to JsonSerializer.Deserialize<TValue> for the raw value, then calls TryFromValue. An unrecognised value throws JsonException.
String-Valued Enums¶
Both strategies work with any TValue, including string:
[OptimizedEnumJsonConverter(OptimizedEnumJsonConverterType.ByValue)]
public sealed partial class Color : OptimizedEnum<Color, string>
{
public static readonly Color Red = new("red", nameof(Red));
public static readonly Color Green = new("green", nameof(Green));
public static readonly Color Blue = new("blue", nameof(Blue));
private Color(string value, string name) : base(value, name) { }
}
With ByValue, the JSON value is "red"/"green"/"blue". With ByName, it is "Red"/"Green"/"Blue".
AOT and Trimming Safety¶
Because the generator emits a concrete, non-generic converter for each type, the converter logic itself is entirely reflection-free:
- No
MakeGenericType— the converter type hasTEnumbaked in at generation time - No
Delegate.CreateDelegate TryFromName/TryFromValueare themselves source-generated static dictionary lookups
[JsonConverter(typeof(...))] is stamped on the partial class at compile time, so STJ's own source-generation pipeline (JsonSerializerContext) can see and wire up the converter without reflection.
Converter instantiation
When using JsonSerializer without a JsonSerializerContext, STJ instantiates the converter class via Activator.CreateInstance at startup (once, then caches it). This is standard STJ behaviour and is not specific to this package. To eliminate that last reflection call in NativeAOT scenarios, use a JsonSerializerContext — STJ's source gen will hard-wire the converter creation directly.
Diagnostics¶
The SystemTextJson generator emits its own diagnostics with the OE2xxx prefix. See Diagnostics for details.
Constraints¶
- The class must inherit from
OptimizedEnum<TEnum, TValue>(OE2001). - The class must be declared
partial(OE2002). - Only one
[OptimizedEnumJsonConverter]per class (enforced byAllowMultiple = falseon the attribute and by[JsonConverter]itself).