使用Golang对同时包含公开和非公开字段的JSON进行编组/解组

5
我可以帮您翻译成中文。这篇文章讨论如何编排/解析包含非公开字段的结构体,但如何处理混合字段呢?
假设有一个结构体:
type Test struct {
    fieldA string `json:"fieldA"`
    FieldB int    `json:"fieldB"`
    FieldC string `json:"fieldC"`
}

我该如何编写MarshalJSON/UnmarshalJSON函数,以便fieldA与FieldB和FieldC一起传输?
以下代码可以编译,但在运行时会溢出调用堆栈。我的猜测是我正在递归地编组对象,但我不确定如何在编码时保留导出和未导出字段。
func (t *Test) MarshalJSON() ([]byte, error) {
    return json.Marshal(struct {
         *Test
         FieldA string `json:"fieldA"`
    }{
         t,
         t.fieldA,
    })
}

func (t *Test) UnmarshalJSON(b []byte) error {
    return json.Unmarshal(b, &t)
}

有没有办法做到这一点?还是我需要重新考虑我的数据结构,也许只是导出该字段?

注意:我知道我可以手动处理每个字段,但如果可能的话,我想避免这样做,以使更新代码更加可管理。

2个回答

14
你可以创建一个特定的结构来处理JSON序列化消息:http://play.golang.org/p/d057T7qfVB
type Test struct {
    fieldA string
    FieldB int
    FieldC string
}

type TestJSON struct {
    FieldA string `json:"fieldA"`
    FieldB int    `json:"fieldB"`
    FieldC string `json:"fieldC"`
}

func (t *Test) MarshalJSON() ([]byte, error) {
    return json.Marshal(TestJSON{
        t.fieldA,
        t.FieldB,
        t.FieldC,
    })
}

func (t *Test) UnmarshalJSON(b []byte) error {
    temp := &TestJSON{}

    if err := json.Unmarshal(b, &temp); err != nil {
        return err
    }

    t.fieldA = temp.FieldA
    t.FieldB = temp.FieldB
    t.FieldC = temp.FieldC

    return nil
}

我不想维护多个结构体,主要是因为它将成为函数中的内联结构体。这对于清晰的代码来说更加优雅和可行。我喜欢它!非常感谢! - KayoticSully

6
另一种解决方案完全可行,但每次更改字段时都需要在许多地方进行更新。这种解决方案更易于维护。
通过使用类型别名和结构嵌入,我们可以创建一个更DRY的解决方案。新字段自动按预期工作,除非它们未公开或需要自定义格式。在这种情况下,只需要进行最少量的工作:在*JSON 结构中列出特殊字段,并在MarshalJSON和UnmarshalJSON 中包含转换表达式。
package main

/*

The Go JSON module can't not access unexported fields in a struct. So
how do you work with them?

This demonstrates the solution in http://choly.ca/post/go-json-marshalling/
where we have a 2nd struct that embeds the primary struct but adds
fields that will be used to expose the unexported fields.  We then write
MarshalJSON() and UnmarshalJSON() functions that do the right thing.

This also helps in situations where we have fields that require a custom
format only in JSON.
*/

import (
    "encoding/json"
    "fmt"
    "time"
)

// Cranberry stores data.
//  Visible: This field is exported and JSON displays it as usual.
//  invisible: This field is unexported but we want it to be included in JSON.
//  Custom: This field has a custom output format.  We store it as time.Time
//    but when it appears in JSON, it should be in Unix Epoch format.
type Cranberry struct {
    Visible   int       `json:"visible"`
    invisible int       // No tag here
    Custom    time.Time `json:"-"` // Don't output this field (we'll handle it in CranberryJSON).
}

// CranberryAlias is an alias of Cranberry. We use an alias because aliases
// are stripped of any functions and we need a struct without
// MarshalJSON/UnmarshalJSON defined, otherwise we'd get a recursive defintion.
type CranberryAlias Cranberry

// CranberryJSON represents out we represent Cranberry to the JSON package.
type CranberryJSON struct {
    *CranberryAlias       // All the exported fields.
    Invisible       int   `json:"invisible"`
    CustomUnixEpoch int64 `json:"epoch"`
    // FYI: The json tags "invisble" and "epoch" can be any valid JSON tag.
    // It is all a matter of how we want the JSON to be presented externally.
}

// MarshalJSON marshals a Cranberry. (struct to JSON)
func (u *Cranberry) MarshalJSON() ([]byte, error) {
    return json.Marshal(&CranberryJSON{
        CranberryAlias: (*CranberryAlias)(u),
        // Unexported or custom-formatted fields are listed here:
        Invisible:       u.invisible,
        CustomUnixEpoch: u.Custom.Unix(),
    })
}

// UnmarshalJSON unmarshals a Cranberry. (JSON to struct)
func (u *Cranberry) UnmarshalJSON(data []byte) error {
    temp := &CranberryJSON{
        CranberryAlias: (*CranberryAlias)(u),
    }
    if err := json.Unmarshal(data, &temp); err != nil {
        return err
    }

    // Copy the exported fields:
    *u = (Cranberry)(*(temp).CranberryAlias)
    // Each unexported field must be copied and/or converted individually:
    u.invisible = temp.Invisible
    u.Custom = time.Unix(temp.CustomUnixEpoch, 0) // Convert while copying.

    return nil
}

func main() {
    var out []byte
    var err error

    // Demonstration of marshalling: Marshal s (struct) to out ([]byte)
    fmt.Printf("Struct to JSON:\n")
    s := &Cranberry{Visible: 1, invisible: 2, Custom: time.Unix(1521492409, 0)}
    out, err = json.Marshal(s)
    if err != nil {
        panic(err)
    }
    fmt.Printf("      got=%v\n", string(out))
    fmt.Println(` expected={"visible":1,"invisible":2,"epoch":1521492409}`)

    // Demonstration of how to unmarshal: Unmarshal out ([]byte) to n (struct)
    fmt.Printf("JSON to struct:\n")
    var n = &Cranberry{}
    err = json.Unmarshal(out, n)
    if err != nil {
        panic(err)
    }
    fmt.Printf("      got=%+v\n", n)
    fmt.Println(` expected=&{Visible:1 invisible:2 Custom:2018-03-19 xx:46:49 xxxxx xxx}`)
}

输出结果如下所示:
$ go run minimal.go 
Struct to JSON:
      got={"visible":1,"invisible":2,"epoch":1521492409}
 expected={"visible":1,"invisible":2,"epoch":1521492409}
JSON to struct:
      got=&{Visible:1 invisible:2 Custom:2018-03-19 16:46:49 -0400 EDT}
 expected=&{Visible:1 invisible:2 Custom:2018-03-19 xx:46:49 xxxxx xxx}

我从http://choly.ca/post/go-json-marshalling/得到了这个想法,所有功劳应归给他。同时也要感谢@anderson-nascente带领我发现了这篇博客文章。


当将Cranberry嵌入到不同类型中或使用类型别名如type OtherBerry CranBerry并进行编组时,这种方法是否也适用? - bjrnt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接