Skip to content

Commit 9af3528

Browse files
committed
feat(codec): Dual marshal preserve RawJSON
1 parent f1914e7 commit 9af3528

2 files changed

Lines changed: 100 additions & 0 deletions

File tree

internal/codec/raw.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Package codec provides generic helpers for encoding and decoding.
2+
package codec
3+
4+
import (
5+
"encoding/json"
6+
7+
"github.com/amp-labs/connectors/internal/datautils"
8+
)
9+
10+
// RawJSON is a generic wrapper that unmarshals JSON into both a typed
11+
// value and a raw representation.
12+
//
13+
// It performs a *dual unmarshal* operation:
14+
//
15+
// 1. The entire JSON object is decoded into [Raw], a map[string]any
16+
// containing every key–value pair.
17+
//
18+
// 2. The same JSON is decoded into [Data], an instance of type T,
19+
// representing the known structured fields defined by the schema.
20+
//
21+
// This allows connectors to safely enrich or inspect
22+
// payloads that may contain user-defined or forward-compatible fields
23+
// without losing type safety for known properties.
24+
//
25+
// When marshaled back to JSON, RawJSON merges the contents of
26+
// [Data] and [Raw] into a single flattened JSON object. Any updates to
27+
// Data will be reflected in the output while preserving unknown fields
28+
// originally captured in Raw.
29+
//
30+
// Example:
31+
//
32+
// type User struct {
33+
// codec.RawJSON[UserData]
34+
// }
35+
//
36+
// type UserData struct {
37+
// ID string `json:"id"`
38+
// Name string `json:"name"`
39+
// }
40+
type RawJSON[T any] struct {
41+
// Raw stores the full decoded JSON object as a key–value map.
42+
Raw map[string]any `json:"-"`
43+
44+
// Data stores the "typed" portion of the JSON object, decoded into T.
45+
Data T
46+
}
47+
48+
// NewRawJSON constructs a new RawJSON[T] instance from a typed value,
49+
// initializing both the Data and Raw representations.
50+
func NewRawJSON[T any](data T) (*RawJSON[T], error) {
51+
dataBytes, err := json.Marshal(data)
52+
if err != nil {
53+
return nil, err
54+
}
55+
56+
var raw map[string]any
57+
if err := json.Unmarshal(dataBytes, &raw); err != nil {
58+
return nil, err
59+
}
60+
61+
return &RawJSON[T]{
62+
Raw: raw,
63+
Data: data,
64+
}, nil
65+
}
66+
67+
// UnmarshalJSON implements [json.Unmarshaler].
68+
// It decodes the input into both the Raw map and the typed Data field.
69+
func (r *RawJSON[T]) UnmarshalJSON(data []byte) error {
70+
if err := json.Unmarshal(data, &r.Raw); err != nil {
71+
return err
72+
}
73+
74+
return json.Unmarshal(data, &r.Data)
75+
}
76+
77+
// MarshalJSON implements [json.Marshaler].
78+
// It merges the typed Data and the captured Raw values into a single
79+
// flattened JSON object before encoding.
80+
func (r RawJSON[T]) MarshalJSON() ([]byte, error) {
81+
dataBytes, err := json.Marshal(r.Data)
82+
if err != nil {
83+
return nil, err
84+
}
85+
86+
var dataMap map[string]any
87+
if err := json.Unmarshal(dataBytes, &dataMap); err != nil {
88+
return nil, err
89+
}
90+
91+
result, err := datautils.FromMap(r.Raw).DeepCopy()
92+
if err != nil {
93+
return nil, err
94+
}
95+
96+
result.AddMapValues(dataMap)
97+
98+
return json.Marshal(result)
99+
}

internal/codec/record.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Package codec provides generic helpers for encoding and decoding.
12
package codec
23

34
import (

0 commit comments

Comments
 (0)