【问题标题】:How to convert a structure to a public structure with custom field types with MarshalJSON如何使用 MarshalJSON 将结构转换为具有自定义字段类型的公共结构
【发布时间】:2017-08-23 09:37:12
【问题描述】:

我有一个类型“书”,我从返回 json 的不同接口读取。读取 json 并处理数据后,我必须将书籍转换为公共书籍类型以隐藏字段并更改输出格式。

我的问题是,来自同一字段 (ISBN) 的输入类型有时是字符串,有时是 int。我认为最简单的解决方案是使用 json.Number 来解组数据。这行得通 - 但我需要不同字段的传出 json 字符串...

这就是我需要帮助的地方。我将有一个自定义类型,我可以在字段的公共结构中设置它,我想将 output-json-field 设置为字符串。我在示例中将自定义类型命名为“mytype”。 (真实数据是嵌套的,我在输出中有更多字段设置为字符串 - 公共结构中的 id 字段只是一个测试)

我的意思是,它应该看起来像这样 - 还是不是?

func (m *mytype) MarshalJSON() ([]byte, error) {
    ...
}

这是我的示例代码:https://play.golang.org/p/rS9HddzDMp

package main 

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

/* ----------------------------------------------------------------------------------------------------
Definition of the internal Book object (read from input)
-----------------------------------------------------------------------------------------------------*/
type Book struct {
    Id                      json.Number         `json:"id"`
    Revision                int                 `json:"revision"`
    ISBN                    json.Number         `json:"isbn"`
    Title                   string              `json:"title"`
}

/* ----------------------------------------------------------------------------------------------------
Definition of the public Book object
-----------------------------------------------------------------------------------------------------*/
type AliasBook Book
type omit           *struct{}
type mytype         string

type PublicBook struct {
    Id          string          `json:"id"`
    Revision    omit            `json:"revision,omitempty"`
    ISBN        mytype          `json:"isbn"`
    *AliasBook
}

/* ----------------------------------------------------------------------------------------------------
Rendering functions
-----------------------------------------------------------------------------------------------------*/
func (bb *Book) MarshalJSON() ([]byte, error) {
    fmt.Println("---------------MarschalJSON---------------")
    aux := PublicBook{
        Id:         bb.Id.String(),
        AliasBook:  (*AliasBook)(bb),
    }

    return json.Marshal(&aux)
}

func main() {
    var jsonStreams[2][]byte
    // Input ISBN as string
    jsonStreams[0] = []byte(`{"id":"123","revision":1234,"isbn":"978-3-86680-192-9","title":"Go for dummies"}`)
    // Input ISBN as int
    jsonStreams[1] = []byte(`{"id":123,"revision":1234,"isbn":9783866801929,"title":"Go for dummies"}`)

    // For each stream
    for i := range jsonStreams {
        fmt.Print("stream: ")
        fmt.Println(string(jsonStreams[i]))

        // Read Input
        b := Book{}
        err := json.Unmarshal(jsonStreams[i], &b)
        if err == nil {
            fmt.Printf("%+v\n", b)
        } else {
            fmt.Println(err)
            fmt.Printf("%+v\n", b)
        }

        // Output as JSON
        response := new(bytes.Buffer)
        enc := json.NewEncoder(response)
        enc.SetEscapeHTML(false)
        enc.SetIndent("", "    ")
        err = enc.Encode(&b)
        if err == nil {
            fmt.Printf("%+v\n", response)
        } else {
            fmt.Println(err)
            fmt.Printf("%+v\n", response)
        }
    }
}

编辑 我有一个适合我的解决方案。 https://play.golang.org/p/Vr4eELsHs1

关键点是,我必须采用“fmt.Sprint(*isbn) 来返回封送器中的字符串。我创建了一个新类型,使用 json.Number 函数将输入转换为 int64 并使用json 自定义封送器到字符串。

【问题讨论】:

    标签: json go interface marshalling


    【解决方案1】:

    最简单的解决方案是使用表示 ISBN 编号的自定义类型。然后,您可以实现自定义 JSON 解码功能,以便您可以解析字符串和数字输入。例如

    type isbn string
    
    func (s *isbn) UnmarshalJSON(buf []byte) error {
        // Read numeric characters only from raw JSON input. This will handle strings, numbers or null etc and strip any
        // optional separators.
        out := make([]byte, 0, len(buf))
        for _, b := range buf {
            if b >= '0' && b <= '9' {
                out = append(out, b)
            }
        }
    
        // Validate ISBN (assuming not using old 10 digit ISBN)
        l := len(out)
        if l != 13 {
            return errors.New("Invalid ISBN length")
        }
        // Calculate check digit and ensure valid.
    
        // Create formatted output. This assumes 13 characters for simplicity
        *s = isbn(fmt.Sprintf("%s-%s-%s-%s-%s", out[:3], out[3:4], out[4:9], out[9:12], out[12:]))
        return nil
    }
    

    上面只是以适合输出的格式存储 ISBN。但是,您可以以任何格式存储,并在需要时使用单独的 json.Marshaler 实现来格式化输出。

    然后您可以像往常一样简单地将其设置为 Book 中的字段:

    type Book struct {
        Id       json.Number `json:"id"`
        Revision int         `json:"revision"`
        ISBN     isbn        `json:"isbn"`
        Title    string      `json:"title"`
    }
    

    上面的 ISBN 解码示例仅用于说明目的。您应该创建一个经过单元测试的完整实现,以确保它正确处理所有预期的输入并在空/格式错误的输入上引发适当的错误。如果这是一个问题,也可以提高性能。

    编辑

    您不能在 json.Marshaler 实现中使用相同的变量调用 json.Marshal。这将导致无限递归循环,例如

    json.Marshal(e) -> e.MarshalJSON -> json.Marshal(e) -> e.MarshalJSON ...

    json.Number 类型是数字的字符串表示形式。如果您只是想将所有数字输出为字符串,则根本不需要任何自定义类型。只需在代码中使用相关的字符串值。例如:

    type PublicBook struct {
        Id          string          `json:"id"`
        // all other fields...
    }
    
    // Creates the public book representation from a book
    func Public(b *Book) *PublicBook {
      return &PublicBook{
         Id: string(b.Id),
      }
    }
    

    这将始终输出一个字符串,因为您使用的是 string 类型,而不是具有自定义 JSON 编组/解组实现的 json.Number 类型。

    【讨论】:

    • 感谢马丁的回复! ISBN 只是我的问题的一个例子。我认为 Book 结构中不需要特殊类型。因为 unmarshal 使用 json.Number 工作。但是我需要一个解组函数,它每次都将我的字段中的值(我用“mytype”标记为字符串)返回。在这种情况下,它是 ISBN 字段。我开始写一个函数,但我得到错误,或者 emtpy 输出或者进入一个循环......你知道,我应该怎么写?示例: type mytype string func (e mytype) MarshalJSON() ([]byte, error) { fmt.Printf("bla: %v\n", e) return json.Marshal(e) }
    • 我明白了,您问的是始终输出字符串的一般情况。我已经更新了我的答案。
    • 谢谢!我有一个带有不同 json.Number 字段的大型嵌套结构。我只需要定义一个私有结构和一个公共结构,并且不会再次在输出中构建整个结构来对我需要的每个字段进行字符串化。这就是为什么我想有一个我只需要在公共结构中定义的自定义类型。这种类型的每个字段都应该是输出中的字符串。你知道我的意思吗?
    • 但是当您可以简单地使用 string 类型本身时,为什么还要创建自定义类型呢?如果您愿意,您仍然可以按照上面的 MarshalJSON 书进行组合方法,您已经将 ID 输出为字符串。
    • 因为在这种情况下,我需要定义公共类型两次:“type public struct”,然后再次在输出中分配值。因此,如果可以在公共结构中只定义一个自定义类型,我就不必在输出中分配每个值,因为 marshal 函数应该为我做这些。
    【解决方案2】:

    我有一个适合我的解决方案。 https://play.golang.org/p/Vr4eELsHs1

    关键点是,我必须使用fmt.Sprint(*isbn) 来返回封送器中的字符串。我创建了一个新类型,使用 json.Number 函数将输入转换为 int64 并使用 json 自定义封送器将其转换为字符串。

    感谢您的帮助!

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-03-19
      • 2021-08-14
      • 2017-08-31
      • 1970-01-01
      • 1970-01-01
      • 2021-09-25
      相关资源
      最近更新 更多