@@ -29,34 +29,44 @@ namespace CloudNative.CloudEvents.SystemTextJson
29
29
/// nor "data_base64" property is populated in a structured mode message.
30
30
/// </description></item>
31
31
/// <item><description>
32
- /// If the data content type is absent or has a media type of "application/json", the data is encoded as JSON,
32
+ /// If the data value is a byte array, it is serialized either directly as binary data
33
+ /// (for binary mode messages) or as base64 data (for structured mode messages).
34
+ /// </description></item>
35
+ /// <item><description>
36
+ /// Otherwise, if the data content type is absent or has a media type indicating JSON, the data is encoded as JSON,
33
37
/// using the <see cref="JsonSerializerOptions"/> passed into the constructor, or the default options.
34
38
/// </description></item>
35
39
/// <item><description>
36
40
/// Otherwise, if the data content type has a media type beginning with "text/" and the data value is a string,
37
41
/// the data is serialized as a string.
38
42
/// </description></item>
39
43
/// <item><description>
40
- /// Otherwise, if the data value is a byte array, it is serialized either directly as binary data
41
- /// (for binary mode messages) or as base64 data (for structured mode messages).
42
- /// </description></item>
43
- /// <item><description>
44
44
/// Otherwise, the encoding operation fails.
45
45
/// </description></item>
46
46
/// </list>
47
47
/// <para>
48
- /// When decoding CloudEvent data, this implementation uses the following rules:
49
- /// </para>
50
- /// <para>
51
- /// In a structured mode message, any data is either binary data within the "data_base64" property value,
52
- /// or is a JSON token as the "data" property value. Binary data is represented as a byte array.
53
- /// A JSON token is decoded as a string if is just a string value and the data content type is specified
54
- /// and has a media type beginning with "text/". A JSON token representing the null value always
55
- /// leads to a null data result. In any other situation, the JSON token is preserved as a <see cref="JsonElement"/>
56
- /// that can be used for further deserialization (e.g. to a specific CLR type). This behavior can be modified
57
- /// by overriding <see cref="DecodeStructuredModeDataBase64Property(JsonElement, CloudEvent)"/> and
58
- /// <see cref="DecodeStructuredModeDataProperty(JsonElement, CloudEvent)"/>.
48
+ /// When decoding structured mode CloudEvent data, this implementation uses the following rules,
49
+ /// which can be modified by overriding <see cref="DecodeStructuredModeDataBase64Property(JsonElement, CloudEvent)"/>
50
+ /// and <see cref="DecodeStructuredModeDataProperty(JsonElement, CloudEvent)"/>.
59
51
/// </para>
52
+ /// <list type="bullet">
53
+ /// <item><description>
54
+ /// If the "data_base64" property is present, its value is decoded as a byte array.
55
+ /// </description></item>
56
+ /// <item><description>
57
+ /// If the "data" property is present (and non-null) and the content type is absent or indicates a JSON media type,
58
+ /// the JSON token present in the property is preserved as a <see cref="JsonElement"/> that can be used for further
59
+ /// deserialization (e.g. to a specific CLR type).
60
+ /// </description></item>
61
+ /// <item><description>
62
+ /// If the "data" property has a string value and a non-JSON content type has been specified, the data is
63
+ /// deserialized as a string.
64
+ /// </description></item>
65
+ /// <item><description>
66
+ /// If the "data" property has a non-null, non-string value and a non-JSON content type has been specified,
67
+ /// the deserialization operation fails.
68
+ /// </description></item>
69
+ /// </list>
60
70
/// <para>
61
71
/// In a binary mode message, the data is parsed based on the content type of the message. When the content
62
72
/// type is absent or has a media type of "application/json", the data is parsed as JSON, with the result as
@@ -310,6 +320,9 @@ private void PopulateDataFromStructuredEvent(CloudEvent cloudEvent, JsonElement
310
320
}
311
321
else
312
322
{
323
+ // If no content type has been specified, default to application/json
324
+ cloudEvent . DataContentType ??= JsonMediaType ;
325
+
313
326
DecodeStructuredModeDataProperty ( dataElement , cloudEvent ) ;
314
327
}
315
328
}
@@ -347,8 +360,9 @@ protected virtual void DecodeStructuredModeDataBase64Property(JsonElement dataBa
347
360
/// </summary>
348
361
/// <remarks>
349
362
/// <para>
350
- /// This implementation converts JSON string tokens to strings when the content type suggests
351
- /// that's appropriate, but otherwise returns the token directly.
363
+ /// This implementation will populate the Data property with the verbatim <see cref="JsonElement"/> if
364
+ /// the content type is deemed to be JSON according to <see cref="IsJsonMediaType(string)"/>. Otherwise,
365
+ /// it validates that the token is a string, and the Data property is populated with that string.
352
366
/// </para>
353
367
/// <para>
354
368
/// Override this method to provide more specialized conversions.
@@ -358,12 +372,24 @@ protected virtual void DecodeStructuredModeDataBase64Property(JsonElement dataBa
358
372
/// not have a null token type.</param>
359
373
/// <param name="cloudEvent">The event being decoded. This should not be modified except to
360
374
/// populate the <see cref="CloudEvent.Data"/> property, but may be used to provide extra
361
- /// information such as the data content type. Will not be null.</param>
375
+ /// information such as the data content type. Will not be null, and the <see cref="CloudEvent.DataContentType"/>
376
+ /// property will be non-null.</param>
362
377
/// <returns>The data to populate in the <see cref="CloudEvent.Data"/> property.</returns>
363
- protected virtual void DecodeStructuredModeDataProperty ( JsonElement dataElement , CloudEvent cloudEvent ) =>
364
- cloudEvent . Data = dataElement . ValueKind == JsonValueKind . String && cloudEvent . DataContentType ? . StartsWith ( "text/" ) == true
365
- ? dataElement . GetString ( )
366
- : ( object ) dataElement . Clone ( ) ; // Deliberately cast to object to provide the conditional operator expression type.
378
+ protected virtual void DecodeStructuredModeDataProperty ( JsonElement dataElement , CloudEvent cloudEvent )
379
+ {
380
+ if ( IsJsonMediaType ( cloudEvent . DataContentType ! ) )
381
+ {
382
+ cloudEvent . Data = dataElement . Clone ( ) ;
383
+ }
384
+ else
385
+ {
386
+ if ( dataElement . ValueKind != JsonValueKind . String )
387
+ {
388
+ throw new ArgumentException ( "CloudEvents with a non-JSON datacontenttype can only have string data values." ) ;
389
+ }
390
+ cloudEvent . Data = dataElement . GetString ( ) ;
391
+ }
392
+ }
367
393
368
394
/// <inheritdoc />
369
395
public override ReadOnlyMemory < byte > EncodeBatchModeMessage ( IEnumerable < CloudEvent > cloudEvents , out ContentType contentType )
@@ -426,12 +452,16 @@ private void WriteCloudEventForBatchOrStructuredMode(Utf8JsonWriter writer, Clou
426
452
default :
427
453
writer . WriteStringValue ( attribute . Type . Format ( value ) ) ;
428
454
break ;
429
-
430
455
}
431
456
}
432
457
433
458
if ( cloudEvent . Data is object )
434
459
{
460
+ if ( cloudEvent . DataContentType is null )
461
+ {
462
+ writer . WritePropertyName ( cloudEvent . SpecVersion . DataContentTypeAttribute . Name ) ;
463
+ writer . WriteStringValue ( JsonMediaType ) ;
464
+ }
435
465
EncodeStructuredModeData ( cloudEvent , writer ) ;
436
466
}
437
467
writer . WriteEndObject ( ) ;
@@ -452,26 +482,31 @@ private void WriteCloudEventForBatchOrStructuredMode(Utf8JsonWriter writer, Clou
452
482
/// <param name="writer"/>The writer to serialize the data to. Will not be null.</param>
453
483
protected virtual void EncodeStructuredModeData ( CloudEvent cloudEvent , Utf8JsonWriter writer )
454
484
{
455
- ContentType dataContentType = new ContentType ( cloudEvent . DataContentType ?? JsonMediaType ) ;
456
- if ( dataContentType . MediaType == JsonMediaType )
457
- {
458
- writer . WritePropertyName ( DataPropertyName ) ;
459
- JsonSerializer . Serialize ( writer , cloudEvent . Data , SerializerOptions ) ;
460
- }
461
- else if ( cloudEvent . Data is string text && dataContentType . MediaType . StartsWith ( "text/" ) )
462
- {
463
- writer . WritePropertyName ( DataPropertyName ) ;
464
- writer . WriteStringValue ( text ) ;
465
- }
466
- else if ( cloudEvent . Data is byte [ ] binary )
485
+ // Binary data is encoded using the data_base64 property, regardless of content type.
486
+ // TODO: Support other forms of binary data, e.g. ReadOnlyMemory<byte>
487
+ if ( cloudEvent . Data is byte [ ] binary )
467
488
{
468
489
writer . WritePropertyName ( DataBase64PropertyName ) ;
469
490
writer . WriteStringValue ( Convert . ToBase64String ( binary ) ) ;
470
491
}
471
492
else
472
493
{
473
- // We assume CloudEvent.Data is not null due to the way this is called.
474
- throw new ArgumentException ( $ "{ nameof ( JsonEventFormatter ) } cannot serialize data of type { cloudEvent . Data ! . GetType ( ) } with content type '{ cloudEvent . DataContentType } '") ;
494
+ ContentType dataContentType = new ContentType ( cloudEvent . DataContentType ?? JsonMediaType ) ;
495
+ if ( IsJsonMediaType ( dataContentType . MediaType ) )
496
+ {
497
+ writer . WritePropertyName ( DataPropertyName ) ;
498
+ JsonSerializer . Serialize ( writer , cloudEvent . Data , SerializerOptions ) ;
499
+ }
500
+ else if ( cloudEvent . Data is string text && dataContentType . MediaType . StartsWith ( "text/" ) )
501
+ {
502
+ writer . WritePropertyName ( DataPropertyName ) ;
503
+ writer . WriteStringValue ( text ) ;
504
+ }
505
+ else
506
+ {
507
+ // We assume CloudEvent.Data is not null due to the way this is called.
508
+ throw new ArgumentException ( $ "{ nameof ( JsonEventFormatter ) } cannot serialize data of type { cloudEvent . Data ! . GetType ( ) } with content type '{ cloudEvent . DataContentType } '") ;
509
+ }
475
510
}
476
511
}
477
512
@@ -484,8 +519,14 @@ public override ReadOnlyMemory<byte> EncodeBinaryModeEventData(CloudEvent cloudE
484
519
{
485
520
return Array . Empty < byte > ( ) ;
486
521
}
522
+ // Binary data is left alone, regardless of the content type.
523
+ // TODO: Support other forms of binary data, e.g. ReadOnlyMemory<byte>
524
+ if ( cloudEvent . Data is byte [ ] bytes )
525
+ {
526
+ return bytes ;
527
+ }
487
528
ContentType contentType = new ContentType ( cloudEvent . DataContentType ?? JsonMediaType ) ;
488
- if ( contentType . MediaType == JsonMediaType )
529
+ if ( IsJsonMediaType ( contentType . MediaType ) )
489
530
{
490
531
var encoding = MimeUtilities . GetEncoding ( contentType ) ;
491
532
if ( encoding is UTF8Encoding )
@@ -501,10 +542,6 @@ public override ReadOnlyMemory<byte> EncodeBinaryModeEventData(CloudEvent cloudE
501
542
{
502
543
return MimeUtilities . GetEncoding ( contentType ) . GetBytes ( text ) ;
503
544
}
504
- if ( cloudEvent . Data is byte [ ] bytes )
505
- {
506
- return bytes ;
507
- }
508
545
throw new ArgumentException ( $ "{ nameof ( JsonEventFormatter ) } cannot serialize data of type { cloudEvent . Data . GetType ( ) } with content type '{ cloudEvent . DataContentType } '") ;
509
546
}
510
547
@@ -541,6 +578,15 @@ public override void DecodeBinaryModeEventData(ReadOnlyMemory<byte> body, CloudE
541
578
cloudEvent . Data = body . ToArray ( ) ;
542
579
}
543
580
}
581
+
582
+ /// <summary>
583
+ /// Determines whether the given media type should be handled as JSON.
584
+ /// The default implementation treats anything ending with "/json" or "+json"
585
+ /// as JSON.
586
+ /// </summary>
587
+ /// <param name="mediaType">The media type to check for JSON. Will not be null.</param>
588
+ /// <returns>Whether or not <paramref name="mediaType"/> indicates JSON data.</returns>
589
+ protected virtual bool IsJsonMediaType ( string mediaType ) => mediaType . EndsWith ( "/json" ) || mediaType . EndsWith ( "+json" ) ;
544
590
}
545
591
546
592
/// <summary>
0 commit comments