@@ -30,7 +30,11 @@ namespace CloudNative.CloudEvents.NewtonsoftJson
30
30
/// nor "data_base64" property is populated in a structured mode message.
31
31
/// </description></item>
32
32
/// <item><description>
33
- /// If the data content type is absent or has a media type of "application/json", the data is encoded as JSON.
33
+ /// If the data value is a byte array, it is serialized either directly as binary data
34
+ /// (for binary mode messages) or as base64 data (for structured mode messages).
35
+ /// </description></item>
36
+ /// <item><description>
37
+ /// Otherwise, if the data content type is absent or has a media type indicating JSON, the data is encoded as JSON.
34
38
/// If the data is already a <see cref="JToken"/>, that is serialized directly as JSON. Otherwise, the data
35
39
/// is converted using the <see cref="JsonSerializer"/> passed into the constructor, or a
36
40
/// default serializer.
@@ -40,29 +44,35 @@ namespace CloudNative.CloudEvents.NewtonsoftJson
40
44
/// the data is serialized as a string.
41
45
/// </description></item>
42
46
/// <item><description>
43
- /// Otherwise, if the data value is a byte array, it is serialized either directly as binary data
44
- /// (for binary mode messages) or as base64 data (for structured mode messages).
45
- /// </description></item>
46
- /// <item><description>
47
47
/// Otherwise, the encoding operation fails.
48
48
/// </description></item>
49
49
/// </list>
50
50
/// <para>
51
- /// When decoding CloudEvent data, this implementation uses the following rules:
52
- /// </para>
53
- /// <para>
54
- /// In a structured mode message, any data is either binary data within the "data_base64" property value,
55
- /// or is a JSON token as the "data" property value. Binary data is represented as a byte array.
56
- /// A JSON token is decoded as a string if is just a string value and the data content type is specified
57
- /// and has a media type beginning with "text/". A JSON token representing the null value always
58
- /// leads to a null data result. In any other situation, the JSON token is preserved as a <see cref="JToken"/>
59
- /// that can be used for further deserialization (e.g. to a specific CLR type). This behavior can be modified
60
- /// by overriding <see cref="DecodeStructuredModeDataBase64Property(JToken, CloudEvent)"/> and
61
- /// <see cref="DecodeStructuredModeDataProperty(JToken, CloudEvent)"/>.
51
+ /// When decoding structured mode CloudEvent data, this implementation uses the following rules,
52
+ /// which can be modified by overriding <see cref="DecodeStructuredModeDataBase64Property(JToken, CloudEvent)"/>
53
+ /// and <see cref="DecodeStructuredModeDataProperty(JToken, CloudEvent)"/>.
62
54
/// </para>
55
+ /// <list type="bullet">
56
+ /// <item><description>
57
+ /// If the "data_base64" property is present, its value is decoded as a byte array.
58
+ /// </description></item>
59
+ /// <item><description>
60
+ /// If the "data" property is present (and non-null) and the content type is absent or indicates a JSON media type,
61
+ /// the JSON token present in the property is preserved as a <see cref="JToken"/> that can be used for further
62
+ /// deserialization (e.g. to a specific CLR type).
63
+ /// </description></item>
64
+ /// <item><description>
65
+ /// If the "data" property has a string value and a non-JSON content type has been specified, the data is
66
+ /// deserialized as a string.
67
+ /// </description></item>
68
+ /// <item><description>
69
+ /// If the "data" property has a non-null, non-string value and a non-JSON content type has been specified,
70
+ /// the deserialization operation fails.
71
+ /// </description></item>
72
+ /// </list>
63
73
/// <para>
64
74
/// In a binary mode message, the data is parsed based on the content type of the message. When the content
65
- /// type is absent or has a media type of "application/json" , the data is parsed as JSON, with the result as
75
+ /// type is absent or has a JSON media type, the data is parsed as JSON, with the result as
66
76
/// a <see cref="JToken"/> (or null if the data is empty). When the content type has a media type beginning
67
77
/// with "text/", the data is parsed as a string. In all other cases, the data is left as a byte array.
68
78
/// This behavior can be specialized by overriding <see cref="DecodeBinaryModeEventData(ReadOnlyMemory{byte}, CloudEvent)"/>.
@@ -296,6 +306,9 @@ private void PopulateDataFromStructuredEvent(CloudEvent cloudEvent, JObject jObj
296
306
}
297
307
else
298
308
{
309
+ // If no content type has been specified, default to application/json
310
+ cloudEvent . DataContentType ??= JsonMediaType ;
311
+
299
312
// We know that dataToken must be non-null here, due to the above conditions.
300
313
DecodeStructuredModeDataProperty ( dataToken ! , cloudEvent ) ;
301
314
}
@@ -334,8 +347,9 @@ protected virtual void DecodeStructuredModeDataBase64Property(JToken dataBase64T
334
347
/// </summary>
335
348
/// <remarks>
336
349
/// <para>
337
- /// This implementation converts JSON string tokens to strings when the content type suggests
338
- /// that's appropriate, but otherwise returns the token directly.
350
+ /// This implementation will populate the Data property with the verbatim <see cref="JToken"/> if
351
+ /// the content type is deemed to be JSON according to <see cref="IsJsonMediaType(string)"/>. Otherwise,
352
+ /// it validates that the token is a string, and the Data property is populated with that string.
339
353
/// </para>
340
354
/// <para>
341
355
/// Override this method to provide more specialized conversions.
@@ -345,12 +359,24 @@ protected virtual void DecodeStructuredModeDataBase64Property(JToken dataBase64T
345
359
/// not have a null token type.</param>
346
360
/// <param name="cloudEvent">The event being decoded. This should not be modified except to
347
361
/// populate the <see cref="CloudEvent.Data"/> property, but may be used to provide extra
348
- /// information such as the data content type. Will not be null.</param>
362
+ /// information such as the data content type. Will not be null, and the <see cref="CloudEvent.DataContentType"/>
363
+ /// property will be non-null.</param>
349
364
/// <returns>The data to populate in the <see cref="CloudEvent.Data"/> property.</returns>
350
- protected virtual void DecodeStructuredModeDataProperty ( JToken dataToken , CloudEvent cloudEvent ) =>
351
- cloudEvent . Data = dataToken . Type == JTokenType . String && cloudEvent . DataContentType ? . StartsWith ( "text/" ) == true
352
- ? ( string ? ) dataToken
353
- : ( object ) dataToken ; // Deliberately cast to object to avoid any implicit conversions
365
+ protected virtual void DecodeStructuredModeDataProperty ( JToken dataToken , CloudEvent cloudEvent )
366
+ {
367
+ if ( IsJsonMediaType ( cloudEvent . DataContentType ! ) )
368
+ {
369
+ cloudEvent . Data = dataToken ;
370
+ }
371
+ else
372
+ {
373
+ if ( dataToken . Type != JTokenType . String )
374
+ {
375
+ throw new ArgumentException ( "CloudEvents with a non-JSON datacontenttype can only have string data values." ) ;
376
+ }
377
+ cloudEvent . Data = ( string ? ) dataToken ;
378
+ }
379
+ }
354
380
355
381
/// <inheritdoc />
356
382
public override ReadOnlyMemory < byte > EncodeStructuredModeMessage ( CloudEvent cloudEvent , out ContentType contentType )
@@ -420,6 +446,11 @@ private void WriteCloudEventForBatchOrStructuredMode(JsonWriter writer, CloudEve
420
446
421
447
if ( cloudEvent . Data is object )
422
448
{
449
+ if ( cloudEvent . DataContentType is null )
450
+ {
451
+ writer . WritePropertyName ( cloudEvent . SpecVersion . DataContentTypeAttribute . Name ) ;
452
+ writer . WriteValue ( JsonMediaType ) ;
453
+ }
423
454
EncodeStructuredModeData ( cloudEvent , writer ) ;
424
455
}
425
456
writer . WriteEndObject ( ) ;
@@ -440,26 +471,31 @@ private void WriteCloudEventForBatchOrStructuredMode(JsonWriter writer, CloudEve
440
471
/// <param name="writer"/>The writer to serialize the data to. Will not be null.</param>
441
472
protected virtual void EncodeStructuredModeData ( CloudEvent cloudEvent , JsonWriter writer )
442
473
{
443
- ContentType dataContentType = new ContentType ( cloudEvent . DataContentType ?? JsonMediaType ) ;
444
- if ( dataContentType . MediaType == JsonMediaType )
445
- {
446
- writer . WritePropertyName ( DataPropertyName ) ;
447
- Serializer . Serialize ( writer , cloudEvent . Data ) ;
448
- }
449
- else if ( cloudEvent . Data is string text && dataContentType . MediaType . StartsWith ( "text/" ) )
450
- {
451
- writer . WritePropertyName ( DataPropertyName ) ;
452
- writer . WriteValue ( text ) ;
453
- }
454
- else if ( cloudEvent . Data is byte [ ] binary )
474
+ // Binary data is encoded using the data_base64 property, regardless of content type.
475
+ // TODO: Support other forms of binary data, e.g. ReadOnlyMemory<byte>
476
+ if ( cloudEvent . Data is byte [ ] binary )
455
477
{
456
478
writer . WritePropertyName ( DataBase64PropertyName ) ;
457
479
writer . WriteValue ( Convert . ToBase64String ( binary ) ) ;
458
480
}
459
481
else
460
482
{
461
- // We assume CloudEvent.Data is not null due to the way this is called.
462
- throw new ArgumentException ( $ "{ nameof ( JsonEventFormatter ) } cannot serialize data of type { cloudEvent . Data ! . GetType ( ) } with content type '{ cloudEvent . DataContentType } '") ;
483
+ ContentType dataContentType = new ContentType ( cloudEvent . DataContentType ?? JsonMediaType ) ;
484
+ if ( IsJsonMediaType ( dataContentType . MediaType ) )
485
+ {
486
+ writer . WritePropertyName ( DataPropertyName ) ;
487
+ Serializer . Serialize ( writer , cloudEvent . Data ) ;
488
+ }
489
+ else if ( cloudEvent . Data is string text && dataContentType . MediaType . StartsWith ( "text/" ) )
490
+ {
491
+ writer . WritePropertyName ( DataPropertyName ) ;
492
+ writer . WriteValue ( text ) ;
493
+ }
494
+ else
495
+ {
496
+ // We assume CloudEvent.Data is not null due to the way this is called.
497
+ throw new ArgumentException ( $ "{ nameof ( JsonEventFormatter ) } cannot serialize data of type { cloudEvent . Data ! . GetType ( ) } with content type '{ cloudEvent . DataContentType } '") ;
498
+ }
463
499
}
464
500
}
465
501
@@ -472,8 +508,14 @@ public override ReadOnlyMemory<byte> EncodeBinaryModeEventData(CloudEvent cloudE
472
508
{
473
509
return Array . Empty < byte > ( ) ;
474
510
}
511
+ // Binary data is left alone, regardless of the content type.
512
+ // TODO: Support other forms of binary data, e.g. ReadOnlyMemory<byte>
513
+ if ( cloudEvent . Data is byte [ ] bytes )
514
+ {
515
+ return bytes ;
516
+ }
475
517
ContentType contentType = new ContentType ( cloudEvent . DataContentType ?? JsonMediaType ) ;
476
- if ( contentType . MediaType == JsonMediaType )
518
+ if ( IsJsonMediaType ( contentType . MediaType ) )
477
519
{
478
520
// TODO: Make this more efficient. We could write to a StreamWriter with a MemoryStream,
479
521
// but then we end up with a BOM in most cases, which I suspect we don't want.
@@ -487,10 +529,6 @@ public override ReadOnlyMemory<byte> EncodeBinaryModeEventData(CloudEvent cloudE
487
529
{
488
530
return MimeUtilities . GetEncoding ( contentType ) . GetBytes ( text ) ;
489
531
}
490
- if ( cloudEvent . Data is byte [ ] bytes )
491
- {
492
- return bytes ;
493
- }
494
532
throw new ArgumentException ( $ "{ nameof ( JsonEventFormatter ) } cannot serialize data of type { cloudEvent . Data . GetType ( ) } with content type '{ cloudEvent . DataContentType } '") ;
495
533
}
496
534
@@ -503,7 +541,7 @@ public override void DecodeBinaryModeEventData(ReadOnlyMemory<byte> body, CloudE
503
541
504
542
Encoding encoding = MimeUtilities . GetEncoding ( contentType ) ;
505
543
506
- if ( contentType . MediaType == JsonMediaType )
544
+ if ( IsJsonMediaType ( contentType . MediaType ) )
507
545
{
508
546
if ( body . Length > 0 )
509
547
{
@@ -547,6 +585,15 @@ protected virtual JsonReader CreateJsonReader(Stream stream, Encoding? encoding)
547
585
{
548
586
DateParseHandling = DateParseHandling . None
549
587
} ;
588
+
589
+ /// <summary>
590
+ /// Determines whether the given media type should be handled as JSON.
591
+ /// The default implementation treats anything ending with "/json" or "+json"
592
+ /// as JSON.
593
+ /// </summary>
594
+ /// <param name="mediaType">The media type to check for JSON. Will not be null.</param>
595
+ /// <returns>Whether or not <paramref name="mediaType"/> indicates JSON data.</returns>
596
+ protected virtual bool IsJsonMediaType ( string mediaType ) => mediaType . EndsWith ( "/json" ) || mediaType . EndsWith ( "+json" ) ;
550
597
}
551
598
552
599
/// <summary>
0 commit comments