serializer API

serializer

package

API reference for the serializer package.

T
type

NamingStrategy

NamingStrategy controls how struct field names are transformed during serialization.

core/serializer/serializer.go:13-13
type NamingStrategy int
T
type

ConverterFactory

ConverterFactory creates a custom type converter for serialization.

core/serializer/serializer.go:25-25
type ConverterFactory func() Converter
I
interface

Converter

Converter handles custom type serialization.

core/serializer/serializer.go:28-31
type Converter interface

Methods

Encode
Method

Parameters

v any

Returns

any
error
func Encode(...)
Decode
Method

Parameters

v any

Returns

any
error
func Decode(...)
S
struct

Policy

Policy defines serialization behavior including naming and custom type handling.

core/serializer/serializer.go:34-41
type Policy struct

Methods

Marshal
Method

Marshal serializes v to JSON bytes using the policy.

Parameters

v any

Returns

[]byte
error
func (*Policy) Marshal(v any) ([]byte, error)
{
	data, err := p.toMap(v)
	if err != nil {
		return nil, err
	}
	return json.Marshal(data)
}
Unmarshal
Method

Unmarshal deserializes JSON bytes into v using the policy.

Parameters

data []byte
v any

Returns

error
func (*Policy) Unmarshal(data []byte, v any) error
{
	target := reflect.ValueOf(v)
	if target.Kind() != reflect.Ptr || target.IsNil() {
		return errors.New("serializer: destination must be a non-nil pointer")
	}
	if target.Elem().Kind() != reflect.Struct {
		return json.Unmarshal(data, v)
	}
	var raw map[string]any
	if err := json.Unmarshal(data, &raw); err != nil {
		return err
	}
	return p.fromMap(raw, v)
}

MarshalToString serializes v to a JSON string.

Parameters

v any

Returns

string
error
func (*Policy) MarshalToString(v any) (string, error)
{
	data, err := p.Marshal(v)
	if err != nil {
		return "", err
	}
	return string(data), nil
}
toMap
Method

Parameters

v any

Returns

map[string]any
error
func (*Policy) toMap(v any) (map[string]any, error)
{
	rv := reflect.ValueOf(v)
	if !rv.IsValid() {
		return nil, errors.New("serializer: cannot marshal nil")
	}
	for rv.Kind() == reflect.Ptr {
		if rv.IsNil() {
			return nil, errors.New("serializer: cannot marshal nil pointer")
		}
		rv = rv.Elem()
	}
	if rv.Kind() != reflect.Struct {
		result := make(map[string]any)
		result["value"] = v
		return result, nil
	}

	rt := rv.Type()
	result := make(map[string]any)

	for i := 0; i < rt.NumField(); i++ {
		field := rt.Field(i)
		if !field.IsExported() {
			continue
		}

		fv := rv.Field(i)

		if p.ignoreNil && fv.Kind() == reflect.Ptr && fv.IsNil() {
			continue
		}
		if p.ignoreZero && fv.IsZero() {
			continue
		}

		name, skip := p.fieldName(field)
		if skip {
			continue
		}
		encoded, err := p.encodeValue(fv)
		if err != nil {
			return nil, fmt.Errorf("serializer: field %s: %w", field.Name, err)
		}
		result[name] = encoded
	}
	return result, nil
}
fromMap
Method

Parameters

raw map[string]any
v any

Returns

error
func (*Policy) fromMap(raw map[string]any, v any) error
{
	rv := reflect.ValueOf(v)
	if rv.Kind() != reflect.Ptr || rv.Elem().Kind() != reflect.Struct {
		return json.Unmarshal(mustJSON(raw), v)
	}

	elem := rv.Elem()
	rt := elem.Type()

	for i := 0; i < rt.NumField(); i++ {
		field := rt.Field(i)
		if !field.IsExported() {
			continue
		}

		name, skip := p.fieldName(field)
		if skip {
			continue
		}
		if val, ok := raw[name]; ok {
			fv := elem.Field(i)
			if fv.CanSet() {
				if err := p.decodeValue(fv, val); err != nil {
					return fmt.Errorf("serializer: field %s: %w", field.Name, err)
				}
			}
		}
	}
	return nil
}
fieldName
Method

Parameters

Returns

string
bool
func (*Policy) fieldName(field reflect.StructField) (string, bool)
{
	if tag := field.Tag.Get(p.tagName); tag != "" {
		parts := strings.Split(tag, ",")
		if parts[0] == "-" {
			return "", true
		}
		if parts[0] != "" {
			return parts[0], false
		}
	}
	return transformName(field.Name, p.naming), false
}
encodeValue
Method

Parameters

Returns

any
error
func (*Policy) encodeValue(value reflect.Value) (any, error)
{
	p.mu.RLock()
	factory, ok := p.customTypes[value.Type()]
	p.mu.RUnlock()
	if !ok {
		return value.Interface(), nil
	}
	if factory == nil {
		return nil, errors.New("custom converter factory is nil")
	}
	converter := factory()
	if converter == nil {
		return nil, errors.New("custom converter is nil")
	}
	return converter.Encode(value.Interface())
}
decodeValue
Method

Parameters

value any

Returns

error
func (*Policy) decodeValue(field reflect.Value, value any) error
{
	p.mu.RLock()
	factory, ok := p.customTypes[field.Type()]
	p.mu.RUnlock()
	if ok {
		if factory == nil {
			return errors.New("custom converter factory is nil")
		}
		converter := factory()
		if converter == nil {
			return errors.New("custom converter is nil")
		}
		decoded, err := converter.Decode(value)
		if err != nil {
			return err
		}
		value = decoded
	}
	return setValue(field, value)
}

Fields

Name Type Description
naming NamingStrategy
ignoreNil bool
ignoreZero bool
customTypes map[reflect.Type]ConverterFactory
tagName string
mu sync.RWMutex
T
type

Option

Option configures a Policy.

core/serializer/serializer.go:44-44
type Option func(*Policy)
F
function

New

New creates a Policy with the given options.

Parameters

opts
...Option

Returns

core/serializer/serializer.go:47-57
func New(opts ...Option) *Policy

{
	p := &Policy{
		naming:      PascalCase,
		tagName:     "json",
		customTypes: make(map[reflect.Type]ConverterFactory),
	}
	for _, opt := range opts {
		opt(p)
	}
	return p
}
F
function

WithNaming

WithNaming sets the naming strategy.

Parameters

Returns

core/serializer/serializer.go:60-62
func WithNaming(n NamingStrategy) Option

{
	return func(p *Policy) { p.naming = n }
}
F
function

WithIgnoreNil

WithIgnoreNil skips nil pointers during marshaling.

Returns

core/serializer/serializer.go:65-67
func WithIgnoreNil() Option

{
	return func(p *Policy) { p.ignoreNil = true }
}
F
function

WithIgnoreZero

WithIgnoreZero skips zero values during marshaling.

Returns

core/serializer/serializer.go:70-72
func WithIgnoreZero() Option

{
	return func(p *Policy) { p.ignoreZero = true }
}
F
function

WithTagName

WithTagName sets the struct tag used for field names (default: “json”).

Parameters

tag
string

Returns

core/serializer/serializer.go:75-77
func WithTagName(tag string) Option

{
	return func(p *Policy) { p.tagName = tag }
}
F
function

WithCustomType

WithCustomType registers a custom converter for a specific type.

Parameters

Returns

core/serializer/serializer.go:80-82
func WithCustomType(typ reflect.Type, factory ConverterFactory) Option

{
	return func(p *Policy) { p.customTypes[typ] = factory }
}
F
function

transformName

Parameters

name
string
strategy

Returns

string
core/serializer/serializer.go:210-219
func transformName(name string, strategy NamingStrategy) string

{
	switch strategy {
	case SnakeCase:
		return toSnake(name)
	case CamelCase:
		return toCamel(name)
	default:
		return name
	}
}
F
function

toSnake

Parameters

s
string

Returns

string
core/serializer/serializer.go:221-230
func toSnake(s string) string

{
	var result strings.Builder
	for i, r := range s {
		if i > 0 && r >= 'A' && r <= 'Z' {
			result.WriteByte('_')
		}
		result.WriteRune(r)
	}
	return strings.ToLower(result.String())
}
F
function

toCamel

Parameters

s
string

Returns

string
core/serializer/serializer.go:232-237
func toCamel(s string) string

{
	if len(s) == 0 {
		return s
	}
	return strings.ToLower(s[:1]) + s[1:]
}
F
function

setValue

Parameters

val
any

Returns

error
core/serializer/serializer.go:277-306
func setValue(fv reflect.Value, val any) error

{
	if val == nil {
		switch fv.Kind() {
		case reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
			fv.SetZero()
			return nil
		default:
			return errors.New("cannot assign null")
		}
	}
	rv := reflect.ValueOf(val)
	if rv.Type().AssignableTo(fv.Type()) {
		fv.Set(rv)
		return nil
	}
	if rv.Type().ConvertibleTo(fv.Type()) && isSafeConversion(rv.Kind(), fv.Kind()) {
		fv.Set(rv.Convert(fv.Type()))
		return nil
	}
	data, err := json.Marshal(val)
	if err != nil {
		return err
	}
	target := reflect.New(fv.Type())
	if err := json.Unmarshal(data, target.Interface()); err != nil {
		return err
	}
	fv.Set(target.Elem())
	return nil
}
F
function

isSafeConversion

Parameters

Returns

bool
core/serializer/serializer.go:308-313
func isSafeConversion(from, to reflect.Kind) bool

{
	if from == to {
		return true
	}
	return from == reflect.String && to == reflect.String
}
F
function

mustJSON

Parameters

v
any

Returns

[]byte
core/serializer/serializer.go:315-318
func mustJSON(v any) []byte

{
	data, _ := json.Marshal(v)
	return data
}
T
type

customID

core/serializer/serializer_test.go:10-10
type customID string
S
struct
Implements: Converter

customIDConverter

core/serializer/serializer_test.go:12-12
type customIDConverter struct

Methods

Encode
Method

Parameters

v any

Returns

any
error
func (customIDConverter) Encode(v any) (any, error)
{
	id, ok := v.(customID)
	if !ok {
		return nil, errors.New("unexpected type")
	}
	return "id:" + string(id), nil
}
Decode
Method

Parameters

v any

Returns

any
error
func (customIDConverter) Decode(v any) (any, error)
{
	value, ok := v.(string)
	if !ok || !strings.HasPrefix(value, "id:") {
		return nil, errors.New("invalid custom ID")
	}
	return customID(strings.TrimPrefix(value, "id:")), nil
}
S
struct

sample

core/serializer/serializer_test.go:30-34
type sample struct

Fields

Name Type Description
FirstName string json:"first_name"
LastName string json:"last_name"
Age int json:"age"
F
function

TestPolicy_Marshal_SnakeCase

Parameters

core/serializer/serializer_test.go:36-49
func TestPolicy_Marshal_SnakeCase(t *testing.T)

{
	p := New(WithNaming(SnakeCase), WithTagName("json"))
	s := sample{FirstName: "John", LastName: "Doe", Age: 30}

	data, err := p.Marshal(s)
	if err != nil {
		t.Fatalf("Marshal failed: %v", err)
	}

	got := string(data)
	if !strings.Contains(got, `"first_name":"John"`) || !strings.Contains(got, `"last_name":"Doe"`) || !strings.Contains(got, `"age":30`) {
		t.Errorf("Marshal: got %q", got)
	}
}
F
function

TestPolicy_Unmarshal_SnakeCase

Parameters

core/serializer/serializer_test.go:51-66
func TestPolicy_Unmarshal_SnakeCase(t *testing.T)

{
	p := New(WithNaming(SnakeCase), WithTagName("json"))
	data := []byte(`{"first_name":"Jane","last_name":"Smith","age":25}`)

	var s sample
	if err := p.Unmarshal(data, &s); err != nil {
		t.Fatalf("Unmarshal failed: %v", err)
	}

	if s.FirstName != "Jane" {
		t.Errorf("FirstName: got %q, want %q", s.FirstName, "Jane")
	}
	if s.Age != 25 {
		t.Errorf("Age: got %d, want %d", s.Age, 25)
	}
}
F
function

TestPolicy_IgnoreNil

Parameters

core/serializer/serializer_test.go:68-83
func TestPolicy_IgnoreNil(t *testing.T)

{
	p := New(WithIgnoreNil(), WithTagName("json"))
	type withPtr struct {
		Name  string `json:"name"`
		Score *int   `json:"score"`
	}
	s := withPtr{Name: "test", Score: nil}
	data, err := p.Marshal(s)
	if err != nil {
		t.Fatalf("Marshal failed: %v", err)
	}
	got := string(data)
	if got != `{"name":"test"}` {
		t.Errorf("Marshal with IgnoreNil: got %q", got)
	}
}
F
function

TestPolicy_IgnoreZero

Parameters

core/serializer/serializer_test.go:85-100
func TestPolicy_IgnoreZero(t *testing.T)

{
	p := New(WithIgnoreZero(), WithTagName("json"))
	type withZero struct {
		Name string `json:"name"`
		Age  int    `json:"age"`
	}
	s := withZero{Name: "test", Age: 0}
	data, err := p.Marshal(s)
	if err != nil {
		t.Fatalf("Marshal failed: %v", err)
	}
	got := string(data)
	if got != `{"name":"test"}` {
		t.Errorf("Marshal with IgnoreZero: got %q", got)
	}
}
F
function

TestPolicy_CamelCase

Parameters

core/serializer/serializer_test.go:102-117
func TestPolicy_CamelCase(t *testing.T)

{
	p := New(WithNaming(CamelCase), WithTagName("json"))
	type camel struct {
		FirstName string
		LastName  string
	}
	s := camel{FirstName: "A", LastName: "B"}
	data, err := p.Marshal(s)
	if err != nil {
		t.Fatalf("Marshal failed: %v", err)
	}
	got := string(data)
	if got != `{"firstName":"A","lastName":"B"}` {
		t.Errorf("Marshal CamelCase: got %q", got)
	}
}
F
function

TestPolicyCustomTypeRoundTrip

Parameters

core/serializer/serializer_test.go:119-143
func TestPolicyCustomTypeRoundTrip(t *testing.T)

{
	policy := New(WithCustomType(reflect.TypeOf(customID("")), func() Converter {
		return customIDConverter{}
	}))
	input := struct {
		ID customID `json:"id"`
	}{ID: "42"}

	data, err := policy.Marshal(input)
	if err != nil {
		t.Fatal(err)
	}
	if string(data) != `{"id":"id:42"}` {
		t.Fatalf("Marshal() = %s", data)
	}
	var output struct {
		ID customID `json:"id"`
	}
	if err := policy.Unmarshal(data, &output); err != nil {
		t.Fatal(err)
	}
	if output.ID != "42" {
		t.Fatalf("ID = %q, want 42", output.ID)
	}
}
F
function

TestPolicyHonorsJSONExclusion

Parameters

core/serializer/serializer_test.go:145-157
func TestPolicyHonorsJSONExclusion(t *testing.T)

{
	policy := New()
	data, err := policy.Marshal(struct {
		Name   string `json:"name"`
		Secret string `json:"-"`
	}{Name: "public", Secret: "private"})
	if err != nil {
		t.Fatal(err)
	}
	if string(data) != `{"name":"public"}` {
		t.Fatalf("Marshal() leaked excluded field: %s", data)
	}
}
F
function

TestPolicyRejectsInvalidAssignment

Parameters

core/serializer/serializer_test.go:159-166
func TestPolicyRejectsInvalidAssignment(t *testing.T)

{
	var output struct {
		Count int `json:"count"`
	}
	if err := New().Unmarshal([]byte(`{"count":"wrong"}`), &output); err == nil {
		t.Fatal("Unmarshal() ignored an invalid field value")
	}
}