-
Notifications
You must be signed in to change notification settings - Fork 0
Add data_base64 support and simplify CE JSON marshaling #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,8 +2,14 @@ | |||||||||||||||||||||||||||||
| package cloudevent | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||
| "encoding/base64" | ||||||||||||||||||||||||||||||
| "encoding/json" | ||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||
| "mime" | ||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| "github.com/tidwall/sjson" | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const ( | ||||||||||||||||||||||||||||||
|
|
@@ -98,7 +104,85 @@ type CloudEvent[A any] struct { | |||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // RawEvent is a cloudevent with a json.RawMessage data field. | ||||||||||||||||||||||||||||||
| type RawEvent = CloudEvent[json.RawMessage] | ||||||||||||||||||||||||||||||
| // It supports both "data" and "data_base64" (CloudEvents JSON spec). | ||||||||||||||||||||||||||||||
| type RawEvent struct { | ||||||||||||||||||||||||||||||
| CloudEventHeader | ||||||||||||||||||||||||||||||
| Data json.RawMessage `json:"data,omitempty"` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // DataBase64 is the raw "data_base64" string when the event was received with | ||||||||||||||||||||||||||||||
| // data_base64 (CloudEvents spec). When set, MarshalJSON emits data_base64 for | ||||||||||||||||||||||||||||||
| // round-trip; otherwise wire form is chosen from DataContentType and Data. | ||||||||||||||||||||||||||||||
| DataBase64 string `json:"data_base64,omitempty"` | ||||||||||||||||||||||||||||||
|
Comment on lines
+110
to
+115
|
||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
106
to
+116
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // BytesForSignature returns the bytes that were signed (wire form of data or data_base64). | ||||||||||||||||||||||||||||||
| // Use for signature verification; not the same as Data when the CE used data_base64. | ||||||||||||||||||||||||||||||
| func (r RawEvent) BytesForSignature() []byte { | ||||||||||||||||||||||||||||||
| if r.DataBase64 != "" { | ||||||||||||||||||||||||||||||
| return []byte(r.DataBase64) | ||||||||||||||||||||||||||||||
|
Comment on lines
+118
to
+122
|
||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return r.Data | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // UnmarshalJSON implements json.Unmarshaler so that both "data" and "data_base64" | ||||||||||||||||||||||||||||||
| // are supported; Data is always set to the resolved payload bytes. | ||||||||||||||||||||||||||||||
| func (r *RawEvent) UnmarshalJSON(data []byte) error { | ||||||||||||||||||||||||||||||
| var dataRaw json.RawMessage | ||||||||||||||||||||||||||||||
| var dataBase64 string | ||||||||||||||||||||||||||||||
| header, err := unmarshalCloudEventWithPayload(data, func(d json.RawMessage, b64 string) error { | ||||||||||||||||||||||||||||||
| dataRaw = d | ||||||||||||||||||||||||||||||
| dataBase64 = b64 | ||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| r.CloudEventHeader = header | ||||||||||||||||||||||||||||||
| if dataRaw != nil && dataBase64 != "" { | ||||||||||||||||||||||||||||||
| return fmt.Errorf("cloudevent: both \"data\" and \"data_base64\" present; only one allowed") | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if dataBase64 != "" { | ||||||||||||||||||||||||||||||
| decoded, err := base64.StdEncoding.DecodeString(dataBase64) | ||||||||||||||||||||||||||||||
|
Comment on lines
+141
to
+145
|
||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| r.Data = decoded | ||||||||||||||||||||||||||||||
| r.DataBase64 = dataBase64 | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| r.Data = dataRaw | ||||||||||||||||||||||||||||||
| r.DataBase64 = "" | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
129
to
154
|
||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // IsJSONDataContentType returns true if the MIME type indicates a JSON payload. | ||||||||||||||||||||||||||||||
| // Matches "application/json" and any "+json" suffix type (e.g. "application/cloudevents+json"). | ||||||||||||||||||||||||||||||
| func IsJSONDataContentType(ct string) bool { | ||||||||||||||||||||||||||||||
| parsed, _, err := mime.ParseMediaType(strings.TrimSpace(ct)) | ||||||||||||||||||||||||||||||
| return err == nil && (parsed == "application/json" || strings.HasSuffix(parsed, "+json")) | ||||||||||||||||||||||||||||||
|
Comment on lines
+158
to
+162
|
||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // MarshalJSON implements json.Marshaler. Uses DataContentType to choose wire form: | ||||||||||||||||||||||||||||||
| // application/json -> "data"; otherwise -> "data_base64" (CloudEvents spec). | ||||||||||||||||||||||||||||||
| func (r RawEvent) MarshalJSON() ([]byte, error) { | ||||||||||||||||||||||||||||||
| data, err := json.Marshal(r.CloudEventHeader) | ||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||
| return nil, err | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if len(r.Data) > 0 || r.DataBase64 != "" { | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
| if r.DataBase64 != "" { | ||||||||||||||||||||||||||||||
| data, err = sjson.SetBytes(data, "data_base64", r.DataBase64) | ||||||||||||||||||||||||||||||
| } else if IsJSONDataContentType(r.DataContentType) || (r.DataContentType == "" && json.Valid(r.Data)) { | ||||||||||||||||||||||||||||||
| data, err = sjson.SetRawBytes(data, "data", r.Data) | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
| } else { | |
| } else { | |
| // NOTE: When DataContentType is empty and r.Data happens to be valid JSON, | |
| // we emit a structured JSON "data" field instead of "data_base64". This means | |
| // that for byte slices which are both valid JSON and valid binary data | |
| // (for example: []byte("[1,2,3]")), the wire representation changes based on | |
| // whether DataContentType is set: | |
| // - DataContentType == "" -> "data" | |
| // - DataContentType is non-JSON -> "data_base64" | |
| // | |
| // This is intentional for convenience, but can be surprising and may affect | |
| // strict round-trip guarantees for edge cases. Callers that care about | |
| // treating such payloads as opaque binary data should set a non-JSON | |
| // DataContentType explicitly to force "data_base64". |
Copilot
AI
Feb 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the CloudEvents JSON format specification, "data" and "data_base64" are mutually exclusive attributes - only one should be present in a CloudEvent JSON representation. However, the MarshalJSON implementation here could theoretically allow both to be emitted if the headerToMap is modified externally or if there's a bug.
The current implementation chooses which field to emit based on conditions (lines 163-174), but it doesn't actively prevent both from being set in the output map. While this is unlikely given the current code flow, consider adding a safeguard or assertion to ensure mutual exclusivity is maintained.
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,7 +8,7 @@ import ( | |||||||||||||||
| "github.com/tidwall/sjson" | ||||||||||||||||
| ) | ||||||||||||||||
|
|
||||||||||||||||
| var definedCloudeEventHdrFields = getJSONFieldNames(reflect.TypeOf(CloudEventHeader{})) | ||||||||||||||||
| var definedCloudeEventHdrFields = getJSONFieldNames(reflect.TypeFor[CloudEventHeader]()) | ||||||||||||||||
|
|
||||||||||||||||
| type cloudEventHeader CloudEventHeader | ||||||||||||||||
|
|
||||||||||||||||
|
|
@@ -19,9 +19,8 @@ func (c *CloudEvent[A]) UnmarshalJSON(data []byte) error { | |||||||||||||||
| return err | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // MarshalJSON implements custom JSON marshaling for CloudEventHeader. | ||||||||||||||||
| // MarshalJSON implements custom JSON marshaling for CloudEvent[A]. | ||||||||||||||||
| func (c CloudEvent[A]) MarshalJSON() ([]byte, error) { | ||||||||||||||||
| // Marshal the base struct | ||||||||||||||||
| data, err := json.Marshal(c.CloudEventHeader) | ||||||||||||||||
| if err != nil { | ||||||||||||||||
| return nil, err | ||||||||||||||||
|
|
@@ -42,21 +41,18 @@ func (c *CloudEventHeader) UnmarshalJSON(data []byte) error { | |||||||||||||||
|
|
||||||||||||||||
| // MarshalJSON implements custom JSON marshaling for CloudEventHeader. | ||||||||||||||||
| func (c CloudEventHeader) MarshalJSON() ([]byte, error) { | ||||||||||||||||
| // Marshal the base struct | ||||||||||||||||
| aux := (cloudEventHeader)(c) | ||||||||||||||||
| aux.SpecVersion = SpecVersion | ||||||||||||||||
| data, err := json.Marshal(aux) | ||||||||||||||||
| if err != nil { | ||||||||||||||||
| return nil, err | ||||||||||||||||
| } | ||||||||||||||||
| // Add all extras using sjson] | ||||||||||||||||
| for k, v := range c.Extras { | ||||||||||||||||
| data, err = sjson.SetBytes(data, k, v) | ||||||||||||||||
| if err != nil { | ||||||||||||||||
| return nil, err | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| return data, nil | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
|
|
@@ -90,30 +86,100 @@ func getJSONFieldNames(t reflect.Type) map[string]struct{} { | |||||||||||||||
|
|
||||||||||||||||
| // unmarshalCloudEvent unmarshals the CloudEventHeader and data field. | ||||||||||||||||
| func unmarshalCloudEvent(data []byte, dataFunc func(json.RawMessage) error) (CloudEventHeader, error) { | ||||||||||||||||
| return unmarshalCloudEventWithPayload(data, func(dataRaw json.RawMessage, _ string) error { | ||||||||||||||||
|
||||||||||||||||
| return unmarshalCloudEventWithPayload(data, func(dataRaw json.RawMessage, _ string) error { | |
| return unmarshalCloudEventWithPayload(data, func(dataRaw json.RawMessage, _ string) error { | |
| if dataRaw == nil { | |
| // No "data" field present (possibly only "data_base64"); for typed CloudEvent[A], | |
| // ignore this and do not call dataFunc to avoid unmarshalling nil. | |
| return nil | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sjson is newly imported/used here, which conflicts with the PR goal of removing the tidwall/sjson dependency. If the intent is dependency removal, RawEvent.MarshalJSON needs to be rewritten without sjson (e.g., marshal header to map and set data/data_base64 before json.Marshal).