Skip to content

CloudEvents extension attributes are written with JSON types outside the CloudEvents type system #53

Description

@feO2x

Rationale

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.

The JSON Event Format §2.2 maps those types onto JSON:

CloudEvents JSON
Boolean boolean
Integer 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.

Scope

  • Only context attributes. Numbers inside data are unconstrained JSON; nothing about the event payload or about the problem+json fix in Decimal metadata values are stored as strings and serialize as JSON strings, contradicting the generated OpenAPI schema #52 is affected.
  • 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
  • Nullnull (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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions