From 6dbb6e2ebc6a293f0af92dc184bbe12a1c6e415b Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Mon, 4 May 2026 14:09:03 +0300 Subject: [PATCH] marshaller: add JSON and bytes passthrough marshallers Add two additional typed marshaller implementations alongside the existing YAML one. - Add TypedJSONMarshaller[T] for default encoding/json marshalling. - Add TypedBytesMarshaller as a passthrough TypedMarshaller[[]byte] for values that are already serialized or stored as opaque blobs. --- CHANGELOG.md | 4 ++ marshaller/typed.go | 52 ++++++++++++++++++++ marshaller/typed_test.go | 101 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 151 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2adcaab..05f9918 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. `Get`/`Put`/`Delete`/`Range`/`Watch` implemented as thin `Tx` wrappers, and `Codec[T].Bind` for cheap binding. Multi-codec transactions lower to a single storage call. +- marshaller: `TypedJSONMarshaller[T]` for `encoding/json`-based + marshalling and `TypedBytesMarshaller` passthrough + `TypedMarshaller[[]byte]` for values that are already serialized or + stored as opaque blobs. ### Changed diff --git a/marshaller/typed.go b/marshaller/typed.go index 84040cd..aa942e4 100644 --- a/marshaller/typed.go +++ b/marshaller/typed.go @@ -1,6 +1,8 @@ package marshaller import ( + "encoding/json" + "gopkg.in/yaml.v3" ) @@ -33,3 +35,53 @@ func (m TypedYamlMarshaller[T]) Unmarshal(data []byte) (T, error) { return out, nil } + +// TypedJSONMarshaller is a generic JSON marshaller for typed objects. +type TypedJSONMarshaller[T any] struct{} + +// NewTypedJSONMarshaller creates a new TypedJSONMarshaller for the specified type. +func NewTypedJSONMarshaller[T any]() TypedJSONMarshaller[T] { + return TypedJSONMarshaller[T]{} +} + +// Marshal serializes the typed data to JSON format. +func (m TypedJSONMarshaller[T]) Marshal(data T) ([]byte, error) { + marshalled, err := json.Marshal(data) + if err != nil { + return []byte{}, errMarshal(err) + } + + return marshalled, nil +} + +// Unmarshal deserializes JSON data into a typed object. +func (m TypedJSONMarshaller[T]) Unmarshal(data []byte) (T, error) { + var out T + + err := json.Unmarshal(data, &out) + if err != nil { + return zero[T](), errUnmarshal(err) + } + + return out, nil +} + +// TypedBytesMarshaller is a passthrough marshaller for raw []byte payloads. +// It implements TypedMarshaller[[]byte] without performing any encoding, +// useful when values are already serialized or stored as opaque blobs. +type TypedBytesMarshaller struct{} + +// NewTypedBytesMarshaller creates a new TypedBytesMarshaller. +func NewTypedBytesMarshaller() TypedBytesMarshaller { + return TypedBytesMarshaller{} +} + +// Marshal returns the input bytes unchanged. +func (m TypedBytesMarshaller) Marshal(data []byte) ([]byte, error) { + return data, nil +} + +// Unmarshal returns the input bytes unchanged. +func (m TypedBytesMarshaller) Unmarshal(data []byte) ([]byte, error) { + return data, nil +} diff --git a/marshaller/typed_test.go b/marshaller/typed_test.go index 2d115e9..953b0ee 100644 --- a/marshaller/typed_test.go +++ b/marshaller/typed_test.go @@ -9,15 +9,15 @@ import ( ) type TestStruct struct { - Name string `yaml:"name"` - Value int `yaml:"value"` - Tags []string `yaml:"tags,omitempty"` + Name string `json:"name" yaml:"name"` + Value int `json:"value" yaml:"value"` + Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` } type NestedStruct struct { - ID int `yaml:"id"` - Data TestStruct `yaml:"data"` - Active bool `yaml:"active"` + ID int `json:"id" yaml:"id"` + Data TestStruct `json:"data" yaml:"data"` + Active bool `json:"active" yaml:"active"` } func TestTypedYamlMarshaller_New(t *testing.T) { @@ -241,3 +241,92 @@ func TestTypedYamlMarshaller_WithSliceType(t *testing.T) { require.NoError(t, err) require.Equal(t, original, unmarshaled) } + +func TestTypedJSONMarshaller_RoundTrip(t *testing.T) { + t.Parallel() + + marsh := marshaller.NewTypedJSONMarshaller[TestStruct]() + + original := TestStruct{ + Name: "roundtrip", + Value: 99, + Tags: []string{"a", "b", "c"}, + } + + marshaled, err := marsh.Marshal(original) + require.NoError(t, err) + require.JSONEq(t, `{"name":"roundtrip","value":99,"tags":["a","b","c"]}`, string(marshaled)) + + unmarshaled, err := marsh.Unmarshal(marshaled) + require.NoError(t, err) + require.Equal(t, original, unmarshaled) +} + +func TestTypedJSONMarshaller_NestedStruct(t *testing.T) { + t.Parallel() + + marsh := marshaller.NewTypedJSONMarshaller[NestedStruct]() + + original := NestedStruct{ + ID: 1, + Data: TestStruct{ + Name: "nested", + Value: 100, + Tags: nil, + }, + Active: true, + } + + marshaled, err := marsh.Marshal(original) + require.NoError(t, err) + + unmarshaled, err := marsh.Unmarshal(marshaled) + require.NoError(t, err) + require.Equal(t, original, unmarshaled) +} + +func TestTypedJSONMarshaller_Unmarshal_InvalidJSON(t *testing.T) { + t.Parallel() + + marsh := marshaller.NewTypedJSONMarshaller[TestStruct]() + + _, err := marsh.Unmarshal([]byte(`{not json}`)) + require.Error(t, err) + require.Contains(t, err.Error(), "Failed to unmarshal") +} + +func TestTypedBytesMarshaller_Passthrough(t *testing.T) { + t.Parallel() + + marsh := marshaller.NewTypedBytesMarshaller() + + original := []byte{0x00, 0x01, 0x02, 0xff} + + marshaled, err := marsh.Marshal(original) + require.NoError(t, err) + require.Equal(t, original, marshaled) + + unmarshaled, err := marsh.Unmarshal(marshaled) + require.NoError(t, err) + require.Equal(t, original, unmarshaled) +} + +func TestTypedBytesMarshaller_Nil(t *testing.T) { + t.Parallel() + + marsh := marshaller.NewTypedBytesMarshaller() + + marshaled, err := marsh.Marshal(nil) + require.NoError(t, err) + require.Nil(t, marshaled) + + unmarshaled, err := marsh.Unmarshal(nil) + require.NoError(t, err) + require.Nil(t, unmarshaled) +} + +func TestTypedBytesMarshaller_ImplementsTypedMarshaller(t *testing.T) { + t.Parallel() + + var _ marshaller.TypedMarshaller[[]byte] = marshaller.NewTypedBytesMarshaller() +}