22using Microsoft . Extensions . Options ;
33using System ;
44using System . Collections . Generic ;
5+ using System . Globalization ;
56using System . Linq ;
67using System . Text ;
78using System . Text . Json ;
@@ -15,6 +16,9 @@ public enum SerialiserPropertyFormat
1516{
1617 CamelCase ,
1718 SnakeCase ,
19+ CamelCaseUpper ,
20+ KebabCase ,
21+ KeabCaseUpper ,
1822}
1923public record SerialiserOptions ( SerialiserPropertyFormat PropertyFormat , Boolean IgnoreNullValues = true , Boolean WriteIndented = false ) ;
2024
@@ -34,29 +38,30 @@ public class SystemTextJsonSerializer : IStringSerialiser
3438{
3539 private readonly JsonSerializerOptions Options ;
3640
37- public static JsonSerializerOptions GetDefaultJsonSerializerOptions ( ) => new JsonSerializerOptions
38- {
39- DefaultIgnoreCondition = JsonIgnoreCondition . WhenWritingDefault ,
40- PropertyNamingPolicy = JsonNamingPolicy . SnakeCaseLower ,
41- WriteIndented = true ,
42- TypeInfoResolver = new DefaultJsonTypeInfoResolver
43- {
44- Modifiers =
45- {
46- typeInfo =>
47- {
48- String [ ] names = new [ ] { "AggregateId" , "AggregateVersion" , "EventId" , "EventNumber" , "EventTimestamp" , "EventType" } ;
49- List < JsonPropertyInfo > matches = typeInfo . Properties
50- . Where ( p => names . Any ( n => string . Equals ( p . Name , n , StringComparison . OrdinalIgnoreCase ) ) )
51- . ToList ( ) ;
41+ public static JsonSerializerOptions GetDefaultJsonSerializerOptions ( ) {
42+ JsonSerializerOptions options = new JsonSerializerOptions ( ) {
43+ DefaultIgnoreCondition = JsonIgnoreCondition . WhenWritingDefault ,
44+ PropertyNamingPolicy = JsonNamingPolicy . SnakeCaseLower ,
45+ WriteIndented = true ,
46+ NumberHandling = JsonNumberHandling . AllowReadingFromString ,
47+ TypeInfoResolver = new DefaultJsonTypeInfoResolver {
48+ Modifiers = {
49+ typeInfo => {
50+ String [ ] names = new [ ] { "AggregateId" , "AggregateVersion" , "EventId" , "EventNumber" , "EventTimestamp" , "EventType" } ;
51+ List < JsonPropertyInfo > matches = typeInfo . Properties . Where ( p => names . Any ( n => string . Equals ( p . Name , n , StringComparison . OrdinalIgnoreCase ) ) ) . ToList ( ) ;
5252
53- foreach ( JsonPropertyInfo match in matches ) {
54- match . ShouldSerialize = ( _ , _ ) => false ;
53+ foreach ( JsonPropertyInfo match in matches ) {
54+ match . ShouldSerialize = ( _ ,
55+ _ ) => false ;
56+ }
5557 }
5658 }
5759 }
58- }
59- } ;
60+ } ;
61+ options . Converters . Add ( new DateTimeSpaceConverter ( ) ) ;
62+
63+ return options ;
64+ }
6065
6166 public SystemTextJsonSerializer ( JsonSerializerOptions options ) {
6267 Options = options ;
@@ -81,6 +86,9 @@ private JsonSerializerOptions BuildSerialiserOptions(SerialiserOptions serialise
8186 {
8287 SerialiserPropertyFormat . CamelCase => JsonNamingPolicy . CamelCase ,
8388 SerialiserPropertyFormat . SnakeCase => JsonNamingPolicy . SnakeCaseLower ,
89+ SerialiserPropertyFormat . CamelCaseUpper => JsonNamingPolicy . SnakeCaseUpper ,
90+ SerialiserPropertyFormat . KebabCase => JsonNamingPolicy . KebabCaseLower ,
91+ SerialiserPropertyFormat . KeabCaseUpper => JsonNamingPolicy . KebabCaseUpper ,
8492 _ => options . PropertyNamingPolicy
8593 } ;
8694 return options ;
@@ -204,4 +212,49 @@ public static T GetValue<T>(String json, String propertyName, SerialiserOptions
204212 if ( ! IsInitialised ) throw new InvalidOperationException ( NotInitialisedErrorMessage ) ;
205213 return Serializer . GetValue < T > ( json , propertyName ) ;
206214 }
215+ }
216+
217+ public class DateTimeSpaceConverter : JsonConverter < DateTime >
218+ {
219+ private static readonly string [ ] AcceptedFormats = new [ ] {
220+ "yyyy-MM-dd HH:mm:ss" , "yyyy-MM-dd H:mm:ss" , "yyyy-MM-ddTHH:mm:ss" , "yyyy-MM-ddTHH:mm:ss.FFFFFFFK" , "o" // ISO 8601 round-trip
221+ } ;
222+ public override DateTime Read ( ref Utf8JsonReader reader , Type typeToConvert , JsonSerializerOptions options )
223+ {
224+ if ( reader . TokenType == JsonTokenType . Null )
225+ {
226+ return default ;
227+ }
228+
229+ if ( reader . TokenType == JsonTokenType . String )
230+ {
231+ var s = reader . GetString ( ) ;
232+ if ( string . IsNullOrWhiteSpace ( s ) )
233+ return default ;
234+
235+ // Try exact known formats first (handles "2026-05-07 06:03:18")
236+ if ( DateTime . TryParseExact ( s , AcceptedFormats , CultureInfo . InvariantCulture , DateTimeStyles . AssumeLocal | DateTimeStyles . AllowWhiteSpaces , out var dtExact ) )
237+ return dtExact ;
238+
239+ // Fall back to general parse
240+ if ( DateTime . TryParse ( s , CultureInfo . InvariantCulture , DateTimeStyles . AssumeLocal , out var dt ) )
241+ return dt ;
242+
243+ throw new JsonException ( $ "Unable to parse DateTime: '{ s } '.") ;
244+ }
245+
246+ // If JSON contains a number, attempt to treat it as Unix seconds (optional)
247+ if ( reader . TokenType == JsonTokenType . Number && reader . TryGetInt64 ( out long seconds ) )
248+ {
249+ return DateTimeOffset . FromUnixTimeSeconds ( seconds ) . LocalDateTime ;
250+ }
251+
252+ throw new JsonException ( $ "Unexpected token parsing DateTime. Token: { reader . TokenType } ") ;
253+ }
254+
255+ public override void Write ( Utf8JsonWriter writer , DateTime value , JsonSerializerOptions options )
256+ {
257+ // Write in the same "space" format so round-trip matches your input
258+ writer . WriteStringValue ( value . ToString ( "yyyy-MM-dd HH:mm:ss" , CultureInfo . InvariantCulture ) ) ;
259+ }
207260}
0 commit comments