You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
WriteExtensionAttributes in CloudEvents/Writing/Json/JsonCloudEventsExtensions.cs writes every extension attribute through SharedJsonSerialization.Writing.MetadataExtensions.WriteMetadataValue, which emits each MetadataKind in its natural JSON form. That is the right behaviour for the event data payload and for problem+json bodies, but extension attributes are not free-form JSON: CloudEvents defines its own closed type system and maps it onto JSON, and JSON number is reserved for one specific CloudEvents type.
Core spec §2.4 Type System defines exactly seven types — Boolean, Integer, String, Binary, URI, URI-reference, Timestamp — and closes the set:
All context attribute values MUST be of one of the types listed above.
Integer is "a whole number in the range -2,147,483,648 to +2,147,483,647 inclusive." §2.3 Extension Context Attributes binds extensions to the same set:
Extension attributes MUST follow the same naming convention and use the same type system as standard attributes.
number, only the integer component optionally prefixed with a minus sign is permitted
String
string
Binary
string, Base64-encoded binary
URI
string following RFC 3986
URI-reference
string following RFC 3986
Timestamp
string following RFC 3339 (ISO 8601)
So a fractional JSON number is not the valid serialization of any CloudEvents attribute type, and a JSON number outside int32 range is outside the Integer type it would have to be.
Current behaviour per kind
Kind
Emitted as
Conformant
Null
null
Yes — the JSON format allows null for an unset attribute and requires decoders to treat it as omitted
Boolean
true / false
Yes — Boolean
Int64 within ±2,147,483,647
42
Yes — Integer
Int64 outside that range
4398046511104
No — outside the Integer range
Double
19.99
No — no CloudEvents type maps to a fractional number
Decimal
19.99
No — same, and this was a conformant String ("19.99") before #52
Array, Object
—
Rejected before serialization by DefaultCloudEventsAttributeConversionService.ValidateAttributeValue via IsPrimitive
This is not a decimal problem. Double and out-of-range Int64 have behaved this way since CloudEvents support landed; #52 simply moved Decimal from the accidentally conformant column into the same bucket as Double, and ToCloudEvent_ShouldWriteDecimalExtensionAttribute_AsUnquotedNumber now pins that.
Only structured JSON mode, which is the only CloudEvents mode the library writes today. If a binary-mode writer is ever added, attributes become header text there and the same mapping question resurfaces in a second place — worth settling the rule once, here.
Severity in practice is low: receivers are typically lenient about extension attribute types. This matters for the library's portability claim rather than for interop with any particular consumer today.
Proposed direction (open for discussion)
Map through the CloudEvents type system in the extension-attribute writer instead of emitting the metadata kind's natural JSON form:
Boolean → JSON boolean
Int64 within int32 range → JSON number
Null → null (or omit the attribute entirely)
everything else, including Double, Decimal, and out-of-range Int64 → JSON string, using the same invariant-culture text MetadataValue.ToString() already produces
String is the spec's escape hatch for values the type system cannot express, so this loses no information — it only changes how the value is framed on the wire. The alternative is to accept the current behaviour as a deliberate deviation and record it, in which case this issue becomes a documentation change on PortableResultsCloudEventsWriteOptions.
The choice is worth making explicitly rather than inheriting from whichever kind happened to be implemented first.
Acceptance Criteria
A decision is recorded on whether CloudEvents extension attributes are mapped through the CloudEvents type system or deliberately deviate from it.
If mapped: Double, Decimal, and Int64 values outside int32 range serialize as JSON strings in extension attributes, with Boolean, in-range Int64, and Null unchanged.
The rule is applied in one place, so a future MetadataKind cannot pick up the non-conformant default by accident.
ToCloudEvent_ShouldWriteDecimalExtensionAttribute_AsUnquotedNumber is updated or replaced to match the decision.
The kind-to-JSON mapping for extension attributes is covered by a test matrix, including the Int64 range boundary.
Reading an extension attribute back is checked against the chosen representation — a value written as a string must not silently change kind on the way in, or the limitation is documented the way the numeric-token behaviour is documented on MetadataJsonReader.
Test code coverage stays above 95%.
Out of scope
Numbers inside the event data payload, problem+json bodies, HTTP header formatting (#51), and binary content mode.
Rationale
WriteExtensionAttributesinCloudEvents/Writing/Json/JsonCloudEventsExtensions.cswrites every extension attribute throughSharedJsonSerialization.Writing.MetadataExtensions.WriteMetadataValue, which emits eachMetadataKindin its natural JSON form. That is the right behaviour for the eventdatapayload and forproblem+jsonbodies, but extension attributes are not free-form JSON: CloudEvents defines its own closed type system and maps it onto JSON, and JSONnumberis reserved for one specific CloudEvents type.Core spec §2.4 Type System defines exactly seven types —
Boolean,Integer,String,Binary,URI,URI-reference,Timestamp— and closes the set:Integeris "a whole number in the range -2,147,483,648 to +2,147,483,647 inclusive." §2.3 Extension Context Attributes binds extensions to the same set:The JSON Event Format §2.2 maps those types onto JSON:
So a fractional JSON number is not the valid serialization of any CloudEvents attribute type, and a JSON number outside int32 range is outside the
Integertype it would have to be.Current behaviour per kind
Nullnullnullfor an unset attribute and requires decoders to treat it as omittedBooleantrue/falseBooleanInt64within ±2,147,483,64742IntegerInt64outside that range4398046511104IntegerrangeDouble19.99Decimal19.99String("19.99") before #52Array,ObjectDefaultCloudEventsAttributeConversionService.ValidateAttributeValueviaIsPrimitiveThis is not a decimal problem.
Doubleand out-of-rangeInt64have behaved this way since CloudEvents support landed; #52 simply movedDecimalfrom the accidentally conformant column into the same bucket asDouble, andToCloudEvent_ShouldWriteDecimalExtensionAttribute_AsUnquotedNumbernow pins that.Scope
dataare unconstrained JSON; nothing about the event payload or about theproblem+jsonfix in Decimal metadata values are stored as strings and serialize as JSON strings, contradicting the generated OpenAPI schema #52 is affected.Proposed direction (open for discussion)
Map through the CloudEvents type system in the extension-attribute writer instead of emitting the metadata kind's natural JSON form:
Boolean→ JSON booleanInt64within int32 range → JSON numberNull→null(or omit the attribute entirely)Double,Decimal, and out-of-rangeInt64→ JSON string, using the same invariant-culture textMetadataValue.ToString()already producesStringis the spec's escape hatch for values the type system cannot express, so this loses no information — it only changes how the value is framed on the wire. The alternative is to accept the current behaviour as a deliberate deviation and record it, in which case this issue becomes a documentation change onPortableResultsCloudEventsWriteOptions.The choice is worth making explicitly rather than inheriting from whichever kind happened to be implemented first.
Acceptance Criteria
Double,Decimal, andInt64values outside int32 range serialize as JSON strings in extension attributes, withBoolean, in-rangeInt64, andNullunchanged.MetadataKindcannot pick up the non-conformant default by accident.ToCloudEvent_ShouldWriteDecimalExtensionAttribute_AsUnquotedNumberis updated or replaced to match the decision.Int64range boundary.MetadataJsonReader.Out of scope
Numbers inside the event
datapayload,problem+jsonbodies, HTTP header formatting (#51), and binary content mode.