serializer
packageAPI reference for the serializer
package.
NamingStrategy
NamingStrategy controls how struct field names are transformed during serialization.
type NamingStrategy int
ConverterFactory
ConverterFactory creates a custom type converter for serialization.
type ConverterFactory func() Converter
Policy
Policy defines serialization behavior including naming and custom type handling.
type Policy struct
Methods
Marshal serializes v to JSON bytes using the policy.
Parameters
Returns
func (*Policy) Marshal(v any) ([]byte, error)
{
data, err := p.toMap(v)
if err != nil {
return nil, err
}
return json.Marshal(data)
}
Unmarshal deserializes JSON bytes into v using the policy.
Parameters
Returns
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
Returns
func (*Policy) MarshalToString(v any) (string, error)
{
data, err := p.Marshal(v)
if err != nil {
return "", err
}
return string(data), nil
}
Parameters
Returns
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
}
Parameters
Returns
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
}
Parameters
Returns
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
}
Parameters
Returns
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())
}
Parameters
Returns
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 |
Option
Option configures a Policy.
type Option func(*Policy)
New
New creates a Policy with the given options.
Parameters
Returns
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
}
WithNaming
WithNaming sets the naming strategy.
Parameters
Returns
func WithNaming(n NamingStrategy) Option
{
return func(p *Policy) { p.naming = n }
}
WithIgnoreNil
WithIgnoreNil skips nil pointers during marshaling.
Returns
func WithIgnoreNil() Option
{
return func(p *Policy) { p.ignoreNil = true }
}
Uses
WithIgnoreZero
WithIgnoreZero skips zero values during marshaling.
Returns
func WithIgnoreZero() Option
{
return func(p *Policy) { p.ignoreZero = true }
}
Uses
WithTagName
WithTagName sets the struct tag used for field names (default: “json”).
Parameters
Returns
func WithTagName(tag string) Option
{
return func(p *Policy) { p.tagName = tag }
}
Uses
WithCustomType
WithCustomType registers a custom converter for a specific type.
Parameters
Returns
func WithCustomType(typ reflect.Type, factory ConverterFactory) Option
{
return func(p *Policy) { p.customTypes[typ] = factory }
}
transformName
Parameters
Returns
func transformName(name string, strategy NamingStrategy) string
{
switch strategy {
case SnakeCase:
return toSnake(name)
case CamelCase:
return toCamel(name)
default:
return name
}
}
toSnake
Parameters
Returns
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())
}
toCamel
Parameters
Returns
func toCamel(s string) string
{
if len(s) == 0 {
return s
}
return strings.ToLower(s[:1]) + s[1:]
}
setValue
Parameters
Returns
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
}
isSafeConversion
Parameters
Returns
func isSafeConversion(from, to reflect.Kind) bool
{
if from == to {
return true
}
return from == reflect.String && to == reflect.String
}
mustJSON
Parameters
Returns
func mustJSON(v any) []byte
{
data, _ := json.Marshal(v)
return data
}
customID
type customID string
customIDConverter
type customIDConverter struct
Methods
Parameters
Returns
func (customIDConverter) Encode(v any) (any, error)
{
id, ok := v.(customID)
if !ok {
return nil, errors.New("unexpected type")
}
return "id:" + string(id), nil
}
Parameters
Returns
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
}
sample
type sample struct
Fields
| Name | Type | Description |
|---|---|---|
| FirstName | string | json:"first_name" |
| LastName | string | json:"last_name" |
| Age | int | json:"age" |
TestPolicy_Marshal_SnakeCase
Parameters
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)
}
}
TestPolicy_Unmarshal_SnakeCase
Parameters
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)
}
}
TestPolicy_IgnoreNil
Parameters
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)
}
}
TestPolicy_IgnoreZero
Parameters
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)
}
}
TestPolicy_CamelCase
Parameters
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)
}
}
TestPolicyCustomTypeRoundTrip
Parameters
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)
}
}
TestPolicyHonorsJSONExclusion
Parameters
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)
}
}
TestPolicyRejectsInvalidAssignment
Parameters
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")
}
}