JSON Serialization Customizations¶
Verified by tests
LenientDateTimeOffsetConverterTests, JsonContextRegistryTests, MessageJsonContextGeneratorTests — library CI run #31657041675 (2026-08-13)
Whizbang uses AOT-compatible JSON serialization via source-generated JsonTypeInfo factories. This page documents custom converters and type handling for edge cases that System.Text.Json does not handle by default.
Overview¶
When data flows through MessageJsonContext (especially for polymorphic models stored as JSONB), custom handling is required for:
- Database-specific timestamp formats (PostgreSQL infinity values)
- Timestamps without timezone offsets
- Array types discovered in message properties
- Nullable enum types
When Customizations Apply¶
- Polymorphic models using
Property().HasColumnType("jsonb")instead ofComplexProperty().ToJson() - Message/event serialization through
JsonContextRegistry - Any type resolved via the generated
MessageJsonContext
JsonContextRegistry is the cross-assembly registry: each generated MessageJsonContext (and framework contexts) registers its IJsonTypeInfoResolver and converters at module-initialization time, and JsonContextRegistry.CreateCombinedOptions() builds the combined JsonSerializerOptions that all Whizbang serialization flows through. Polymorphic types are serialized with a generator-hardcoded $type discriminator property whose values are the type's SimpleName (see JsonContextSnippets.cs, TypeDiscriminatorPropertyName = "$type").
Custom Converters¶
LenientDateTimeOffsetConverter¶
Handles DateTimeOffset values that do not conform to strict ISO 8601 format, particularly from PostgreSQL JSONB storage.
LenientDateTimeOffsetConverter
// Supports various input formats:
// - ISO 8601 with offset: "2024-01-15T10:30:00+05:00"
// - Zulu time: "2024-01-15T10:30:00Z"
// - No timezone (assumes UTC): "2024-01-15T10:30:00"
// - Date only: "2024-01-15"
// - PostgreSQL special values: "-infinity", "infinity"
| Input Format | Output | Notes |
|---|---|---|
"2024-01-15T10:30:00+05:00" |
Preserves offset | Standard ISO 8601 with offset |
"2024-01-15T10:30:00Z" |
UTC (offset = 0) | Zulu time |
"2024-01-15T10:30:00" |
UTC (offset = 0) | No timezone - assumes UTC |
"2024-01-15" |
Midnight UTC | Date-only format |
"-infinity" |
DateTimeOffset.MinValue |
PostgreSQL special value |
"infinity" |
DateTimeOffset.MaxValue |
PostgreSQL special value |
"" |
default(DateTimeOffset) |
Empty string |
Database-Specific Notes:
- PostgreSQL: Stores
timestamptzwithout explicit offset in JSONB; uses-infinity/infinityfor unbounded ranges - SQL Server: May have different edge cases (TBD)
- MySQL: May have different edge cases (TBD)
LenientNullableDateTimeOffsetConverter¶
Nullable wrapper for LenientDateTimeOffsetConverter. Handles null JSON values and delegates all other values to the non-nullable converter.
LenientNullableDateTimeOffsetConverter
// Handles:
// - null -> returns null
// - Any valid DateTimeOffset string -> delegates to LenientDateTimeOffsetConverter
Generator-Managed Type Handling¶
ArrayTypeInfo¶
The ArrayTypeInfo record contains information about discovered array types used in messages. This enables the source generator to create JsonTypeInfo<T[]> factories automatically.
ArrayTypeInfo Record
// Example: For a property of type IEvent[]
// ArrayTypeName: "global::Whizbang.Core.IEvent[]"
// ElementTypeName: "global::Whizbang.Core.IEvent"
// ElementSimpleName: "IEvent"
// ElementUniqueIdentifier: "Whizbang_Core_IEvent"
Properties:
| Property | Description | Example |
|---|---|---|
ArrayTypeName |
Fully qualified array type name | "global::Whizbang.Core.IEvent[]" |
ElementTypeName |
Fully qualified element type name | "global::Whizbang.Core.IEvent" |
ElementSimpleName |
Simple element type name | "IEvent" |
ElementUniqueIdentifier |
Sanitized identifier for C# code generation | "Whizbang_Core_IEvent" |
The ElementUniqueIdentifier sanitizes special characters to create valid C# identifiers:
- Strips
global::prefix - Replaces
.,<,>,,with_and removes spaces - Replaces
?with__Nullable
Array Type Discovery¶
When the message JSON context generator encounters array properties, it automatically generates JsonTypeInfo<T[]> factories:
Array Type Discovery
// Message with array property
public record BatchCommand : ICommand {
public Guid[] ItemIds { get; init; } = [];
public IEvent[] Events { get; init; } = [];
}
// Generator creates:
// - CreateArray_System_Guid()
// - CreateArray_Whizbang_Core_IEvent()
Supported Array Types:
| Property Type | Generated Factory |
|---|---|
string[] |
CreateArray_System_String |
int[] |
CreateArray_System_Int32 |
Guid[] |
CreateArray_System_Guid |
int?[] |
CreateArray_System_Int32__Nullable |
CustomType[] |
CreateArray_Namespace_CustomType |
Dictionary<string, string>[] |
CreateArray_System_Collections_Generic_Dictionary_string__string_ |
Nullable Enum Types¶
When an enum type is discovered, the generator automatically creates JsonTypeInfo for both:
EnumType(non-nullable)EnumType?(nullable)
This ensures System.Nullable`1[EnumType] is always available without tracking which enums are used as nullable.
Troubleshooting¶
"JsonTypeInfo metadata for type 'X' was not provided"¶
Cause: The type was not discovered by the generator or does not have a factory.
Check:
1. Is it a nested type? Verify the generator handles CLR name format (Namespace.Container+NestedClass)
2. Is it a nullable enum? Generator should create both versions automatically
3. Is it a custom type? Needs [WhizbangSerializable] or be reachable from a message property
"Unable to parse DateTimeOffset from value: X"¶
Cause: LenientDateTimeOffsetConverter does not handle this format.
Check:
1. What is the actual value? May need to add handling to LenientDateTimeOffsetConverter
2. Which database? May need database-specific handling
3. Add a test case to LenientDateTimeOffsetConverterTests.cs
"Circular type reference detected"¶
Cause: Type A has property of type B, type B has property of type A.
Solution: Use [JsonIgnore] on one property to break the cycle, or use a custom JsonConverter.
Adding New Custom Handling¶
- Create converter in
src/Whizbang.Core/Serialization/ - Add tests in
tests/Whizbang.Core.Tests/Serialization/ - Update generator if needed (snippets in
JsonContextSnippets.cs) - Update this documentation with the new handling
- Link tests using
<tests>tags in code
See Also¶
- Message JSON Context - Generated JSON context overview
- Source Generators - How generators create type info
- AOT Compatibility - Native AOT requirements