如何使用 MarshalJSON 将结构转换为具有自定义字段类型的 public 结构

How to convert a structure to a public structure with custom field types with MarshalJSON

我有一个类型 "Book",我从 returns json 的不同界面读取。阅读 json 和处理数据后,我必须将书籍转换为 public 书籍类型以隐藏字段并更改输出格式。

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

这就是我需要帮助的地方。我会有一个自定义类型,我可以在字段的 public 结构中设置它,我想在其中将输出-json-字段设置为字符串。我在示例中将自定义类型命名为 "mytype"。 (实际数据是嵌套的,我在输出中设置了更多字段 - public 结构中的 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) 转为 return 封送拆收器中的字符串。我创建了一个新类型,使用 json.Number 函数并使用 json 自定义封送拆收器将其转换为字符串。

最简单的解决方案是拥有一个表示 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 解码示例仅供参考。您应该创建一个经过单元测试的完整实现,以确保它正确处理所有预期的输入并在 empty/malformed 输入上引发适当的错误。如果这是一个问题,还可以改进性能。

编辑

您不能在 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 marshal/unmarshal 实现的 json.Number 类型。

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

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

感谢您的帮助!