Skip to content

Instantly share code, notes, and snippets.

@liweitianux
Last active May 23, 2025 01:38
Show Gist options
  • Save liweitianux/11234821d277d7aae668425528549c52 to your computer and use it in GitHub Desktop.
Save liweitianux/11234821d277d7aae668425528549c52 to your computer and use it in GitHub Desktop.
JSON-based marshaling with custom "oplog" tag
package main
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"time"
"strings"
)
type oplogMarshaler interface {
MarshalOpLog() ([]byte, error)
}
type jsonMarshaler interface {
MarshalJSON() ([]byte, error)
}
func Marshal(v any) ([]byte, error) {
if w, ok := v.(oplogMarshaler); ok {
return w.MarshalOpLog()
}
if v == nil {
return []byte("null"), nil
}
buf := &bytes.Buffer{}
err := marshal(buf, reflect.ValueOf(v))
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func marshal(buf *bytes.Buffer, v reflect.Value) error {
switch v.Kind() {
case reflect.Pointer, reflect.Interface:
if v.IsNil() {
buf.WriteString("null")
return nil
}
return marshal(buf, v.Elem())
case reflect.Array, reflect.Slice:
buf.WriteByte('[')
for i := 0; i < v.Len(); i++ {
if i > 0 {
buf.WriteByte(',')
}
if err := marshal(buf, v.Index(i)); err != nil {
return err
}
}
buf.WriteByte(']')
case reflect.Map:
buf.WriteByte('{')
for i, key := range v.MapKeys() {
if i > 0 {
buf.WriteByte(',')
}
if err := marshal(buf, key); err != nil {
return err
}
buf.WriteByte(':')
if err := marshal(buf, v.MapIndex(key)); err != nil {
return err
}
}
buf.WriteByte('}')
case reflect.Struct:
_, err := marshalStruct(buf, v, false)
if err != nil {
return err
}
default:
var b []byte
var err error
if w, ok := v.Interface().(oplogMarshaler); ok {
b, err = w.MarshalOpLog()
} else {
b, err = json.Marshal(v.Interface())
}
if err != nil {
return err
}
buf.Write(b)
}
return nil
}
func marshalStruct(buf *bytes.Buffer, v reflect.Value, embedded bool) (int, error) {
if v.Kind() != reflect.Struct {
return 0, fmt.Errorf("want Struct but got %s", v.Type())
}
{
var b []byte
var err error
done := true
if w, ok := v.Interface().(oplogMarshaler); ok {
b, err = w.MarshalOpLog()
} else if w, ok := v.Interface().(jsonMarshaler); ok {
b, err = w.MarshalJSON()
} else {
done = false
}
if done {
if err != nil {
return 0, err
}
if len(b) == 0 {
return 0, nil
}
buf.Write(b)
return 1, nil
}
}
if !embedded {
buf.WriteByte('{')
}
n := 0
for i := 0; i < v.NumField(); i++ {
f := v.Type().Field(i)
if !f.IsExported() {
continue
}
// Deal with embedded struct.
if f.Anonymous {
w := v.Field(i)
if w.Kind() == reflect.Pointer {
w = w.Elem()
}
if n > 0 {
buf.WriteByte(',')
}
m, err := marshalStruct(buf, w, true)
if err != nil {
return 0, err
}
n += m
continue
}
key, ok := f.Tag.Lookup("oplog")
if !ok {
key = f.Tag.Get("json")
key = strings.Split(key, ",")[0] // ignore ",omitempty" etc.
}
if key == "-" {
continue
}
if key == "" {
key = f.Name
}
if n > 0 {
buf.WriteByte(',')
}
fmt.Fprintf(buf, `"%s":`, key)
if err := marshal(buf, v.Field(i)); err != nil {
return 0, err
}
n++
}
if !embedded {
buf.WriteByte('}')
}
return n, nil
}
// ----------------------------------------------------------------------
type BaseModel struct {
ID uint `json:"id"`
At time.Time `json:"at" oplog:"op_at"`
}
type Model1 struct {
BaseModel
AField string
BField string `json:"-" oplog:"bfield"`
CField string `json:"cfield" oplog:"-"`
DField string `json:"dfield" oplog:""`
Int1 int `json:"int1" oplog:"op_int1"`
Float1 float32 `oplog:"float1"`
}
type SubModel1 struct {
AAA string `json:"aaa"`
BBB string `json:"bbb" oplog:"-"`
}
type Model2 struct {
BaseModel
Sub1 *SubModel1 `json:"sub1"`
Sub2 []SubModel1 `json:"sub2"`
CCC *string `json:"ccc"`
Names []string `json:"names"`
Numbers []int `oplog:"numbers"`
Items map[string]int `json:"items"`
}
type Model3 struct {
BaseModel
}
type Model4 struct {
F1 string
F2 string `oplog:"f2"`
Time time.Time
BaseModel
}
type Model5 struct {
*BaseModel
}
type Model6 struct {
time.Time
}
type Model7 struct {
ABC string
XYZ int
}
func (m Model7) MarshalOpLog() ([]byte, error) {
s := fmt.Sprintf(`{"[abc]":"%s", "[xyz]":%d}`, m.ABC, m.XYZ)
return []byte(s), nil
}
type Model8 struct {
Base Model7 `oplog:"base"`
Hello string
}
func show(name string, v any) {
if b, err := json.Marshal(v); err != nil {
fmt.Printf("*** ERROR: json(%s) failed: %v\n", err)
} else {
fmt.Printf("json(%s): %s\n", name, b)
}
fmt.Println()
if b, err := Marshal(v); err != nil {
fmt.Printf("*** ERROR: oplog(%s) failed: %v\n", err)
} else {
fmt.Printf("oplog(%s): %s\n", name, b)
}
fmt.Println()
}
func main() {
bm := BaseModel{ID: 123, At: time.Now()}
m1 := Model1{
BaseModel: bm,
AField: "aaa",
BField: "bbb",
CField: "ccc",
DField: "ddd",
Int1: 123456,
Float1: 123.456,
}
show("m1", m1)
sm1 := SubModel1{AAA: "a", BBB: "b"}
sm2 := SubModel1{AAA: "hhh", BBB: "yyy"}
str1 := "str1"
m2 := Model2{
BaseModel: bm,
Sub1: &sm1,
Sub2: []SubModel1{sm1, sm2},
CCC: &str1,
Names: []string{"name1", "name2", "name3"},
Numbers: []int{11, 22, 33},
Items: map[string]int{"name1": 11, "name2": 22, "name3": 33},
}
show("m2", m2)
m3 := &Model3{BaseModel: bm}
show("m3", m3)
m4 := Model4{
F1: "field1",
F2: "field2",
Time: time.Now(),
BaseModel: bm,
}
show("m4", m4)
m5 := &Model5{BaseModel: &bm}
show("m5", m5)
m6 := Model6{Time: time.Now()}
show("m6", m6)
m7 := Model7{ABC: "abc", XYZ: 123}
show("m7", m7)
m8 := Model8{
Base: m7,
Hello: "hello",
}
show("m8", m8)
show("nil:untyped", nil)
var n1 *int
show("nil:ptr", n1)
type myint int
var n2 *myint
show("nil:myint", n2)
var n3 any
show("nil:any", n3)
n3 = n2
show("nil:any=", n3)
type iface1 interface{}
var n4 iface1
show("nil:iface1", n4)
type stringer interface {
String() string
}
var n5 stringer
show("nil:stringer", n5)
}
@liweitianux
Copy link
Author

liweitianux commented Apr 2, 2025

Program output:

json(m1): {"id":123,"at":"2025-04-02T17:35:13.51543918+08:00","AField":"aaa","cfield":"ccc","dfield":"ddd","int1":123456,"Float1":123.456}

oplog(m1): {"id":123,"op_at":"2025-04-02T17:35:13.51543918+08:00","AField":"aaa","bfield":"bbb","DField":"ddd","op_int1":123456,"float1":123.456}

json(m2): {"id":123,"at":"2025-04-02T17:35:13.51543918+08:00","sub1":{"aaa":"a","bbb":"b"},"sub2":[{"aaa":"a","bbb":"b"},{"aaa":"hhh","bbb":"yyy"}],"ccc":"str1","names":["name1","name2","name3"],"Numbers":[11,22,33],"items":{"name1":11,"name2":22,"name3":33}}

oplog(m2): {"id":123,"op_at":"2025-04-02T17:35:13.51543918+08:00","sub1":{"aaa":"a"},"sub2":[{"aaa":"a"},{"aaa":"hhh"}],"ccc":"str1","names":["name1","name2","name3"],"numbers":[11,22,33],"items":{"name1":11,"name2":22,"name3":33}}

json(m3): {"id":123,"at":"2025-04-02T17:35:13.51543918+08:00"}

oplog(m3): {"id":123,"op_at":"2025-04-02T17:35:13.51543918+08:00"}

json(m4): {"F1":"field1","F2":"field2","Time":"2025-04-02T17:35:13.515792895+08:00","id":123,"at":"2025-04-02T17:35:13.51543918+08:00"}

oplog(m4): {"F1":"field1","f2":"field2","Time":"2025-04-02T17:35:13.515792895+08:00","id":123,"op_at":"2025-04-02T17:35:13.51543918+08:00"}

json(m5): {"id":123,"at":"2025-04-02T17:35:13.51543918+08:00"}

oplog(m5): {"id":123,"op_at":"2025-04-02T17:35:13.51543918+08:00"}

json(m6): "2025-04-02T17:35:13.51582799+08:00"

oplog(m6): "2025-04-02T17:35:13.51582799+08:00"

json(m7): {"ABC":"abc","XYZ":123}

oplog(m7): {"[abc]":"abc", "[xyz]":123}

json(m8): {"Base":{"ABC":"abc","XYZ":123},"Hello":"hello"}

oplog(m8): {"base":{"[abc]":"abc", "[xyz]":123},"Hello":"hello"}

json(nil:untyped): null

oplog(nil:untyped): null

json(nil:ptr): null

oplog(nil:ptr): null

json(nil:myint): null

oplog(nil:myint): null

json(nil:any): null

oplog(nil:any): null

json(nil:any=): null

oplog(nil:any=): null

json(nil:iface1): null

oplog(nil:iface1): null

json(nil:stringer): null

oplog(nil:stringer): null

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment