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

huangapple go评论130阅读模式
英文:

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

问题

我有一个类型为"Book"的对象,我从不同的接口中读取它,该接口返回JSON格式的数据。在读取JSON并处理数据后,我需要将这本书转换为公共书籍类型,以隐藏字段并更改输出格式。

我的问题是,同一个字段(ISBN)的输入类型有时是字符串,有时是整数。我认为最简单的解决方案是使用json.Number来解组数据。这个方法是有效的,但是我需要在不同的字段上将输出的JSON字段设置为字符串...

这就是我需要帮助的地方。我想在公共结构体中的字段上设置一个自定义类型,以便将输出的JSON字段设置为字符串。在示例中,我将自定义类型命名为"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编组器将其转换为字符串。

英文:

I have a Type "Book" that i read from a different interface which returns json. After reading the json and processing data, i have to convert the book to a public book type to hide fields and change output format.

My problem is, that the input type from the same field (ISBN) is sometimes string and sometimes int. I thought that the easiest solution is to use json.Number to unmarshal the data. That works - but i need string on the outgoing json on different fields...

That is the point where i need help. I would have a custom type which i can set in the public structure at the fields, where i want to set the output-json-field to string. I named the custom type "mytype" in the example. (The real data are nested and i have more fields that i set to string in the output - the id field in the public structure is only a test)

I mean, it should look something like that - or not?

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

Here is my example code: <https://play.golang.org/p/rS9HddzDMp>

package main 
import (  
&quot;encoding/json&quot;
&quot;fmt&quot;
&quot;bytes&quot;
)
/* ----------------------------------------------------------------------------------------------------
Definition of the internal Book object (read from input)
-----------------------------------------------------------------------------------------------------*/
type Book struct {
Id    					json.Number			`json:&quot;id&quot;`
Revision				int					`json:&quot;revision&quot;`
ISBN					json.Number			`json:&quot;isbn&quot;`
Title					string      		`json:&quot;title&quot;`
}
/* ----------------------------------------------------------------------------------------------------
Definition of the public Book object
-----------------------------------------------------------------------------------------------------*/
type AliasBook Book
type omit 			*struct{}
type mytype			string
type PublicBook struct {
Id 			string 			`json:&quot;id&quot;`
Revision 	omit 			`json:&quot;revision,omitempty&quot;`
ISBN	 	mytype			`json:&quot;isbn&quot;`
*AliasBook
}
/* ----------------------------------------------------------------------------------------------------
Rendering functions
-----------------------------------------------------------------------------------------------------*/
func (bb *Book) MarshalJSON() ([]byte, error) {
fmt.Println(&quot;---------------MarschalJSON---------------&quot;)
aux := PublicBook{
Id:			bb.Id.String(),
AliasBook:	(*AliasBook)(bb),
}
return json.Marshal(&amp;aux)
}
func main() {
var jsonStreams[2][]byte
// Input ISBN as string
jsonStreams[0] = []byte(`{&quot;id&quot;:&quot;123&quot;,&quot;revision&quot;:1234,&quot;isbn&quot;:&quot;978-3-86680-192-9&quot;,&quot;title&quot;:&quot;Go for dummies&quot;}`)
// Input ISBN as int
jsonStreams[1] = []byte(`{&quot;id&quot;:123,&quot;revision&quot;:1234,&quot;isbn&quot;:9783866801929,&quot;title&quot;:&quot;Go for dummies&quot;}`)
// For each stream
for i := range jsonStreams {
fmt.Print(&quot;stream: &quot;)
fmt.Println(string(jsonStreams[i]))
// Read Input
b := Book{}
err := json.Unmarshal(jsonStreams[i], &amp;b)
if err == nil {
fmt.Printf(&quot;%+v\n&quot;, b)
} else {
fmt.Println(err)
fmt.Printf(&quot;%+v\n&quot;, b)
}
// Output as JSON
response := new(bytes.Buffer)
enc := json.NewEncoder(response)
enc.SetEscapeHTML(false)
enc.SetIndent(&quot;&quot;, &quot;    &quot;)
err = enc.Encode(&amp;b)
if err == nil {
fmt.Printf(&quot;%+v\n&quot;, response)
} else {
fmt.Println(err)
fmt.Printf(&quot;%+v\n&quot;, response)
}
}
}

Edit
I have a solution which works for me. https://play.golang.org/p/Vr4eELsHs1

The keypoint was, that i have to take "fmt.Sprint(*isbn) to return the string in the marshaler. I created a new type, convert the input to int64 with the json.Number function and convert it with the json custom marshaler to string.

答案1

得分: 1

最简单的解决方案是创建一个自定义类型来表示ISBN号码。然后,你可以实现自定义的JSON解码功能,以便可以解析字符串和数字输入。例如:

type isbn string

func (s *isbn) UnmarshalJSON(buf []byte) error {
    // 从原始的JSON输入中仅读取数字字符。这将处理字符串、数字或null等,并去除任何可选的分隔符。
    out := make([]byte, 0, len(buf))
    for _, b := range buf {
        if b >= '0' && b <= '9' {
            out = append(out, b)
        }
    }

    // 验证ISBN(假设不使用旧的10位ISBN)
    l := len(out)
    if l != 13 {
        return errors.New("Invalid ISBN length")
    }
    // 计算校验位并确保有效。

    // 创建格式化的输出。这里简单地假设有13个字符。
    *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"`
    // 其他所有字段...
}

// 从Book创建公共书籍表示
func Public(b *Book) *PublicBook {
    return &PublicBook{
        Id: string(b.Id),
    }
}

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

英文:

The easiest solution is to have a custom type that represents ISBN numbers. You can then implement custom JSON decoding functionality so that you can parse both string & numeric input. For example

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 &gt;= &#39;0&#39; &amp;&amp; b &lt;= &#39;9&#39; {
out = append(out, b)
}
}
// Validate ISBN (assuming not using old 10 digit ISBN)
l := len(out)
if l != 13 {
return errors.New(&quot;Invalid ISBN length&quot;)
}
// Calculate check digit and ensure valid.
// Create formatted output. This assumes 13 characters for simplicity
*s = isbn(fmt.Sprintf(&quot;%s-%s-%s-%s-%s&quot;, out[:3], out[3:4], out[4:9], out[9:12], out[12:]))
return nil
}

The above just stores the ISBN in a format suitable for output. However, you could store in any format and have a separate json.Marshaler implementation to format the output if this was required.

Then you can simply make this a field in your Book as normal:

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

The ISBN decoding example above is for illustration purposes. You should create a full implementation that is unit tested to ensure it handles all expected input correctly and raises the appropriate errors on empty/malformed input. The performance could also be improved if this was an issue.

EDIT

You can't call json.Marshal inside your json.Marshaler implementation with the same variable. This will cause an infinite recursive loop, e.g.

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

The json.Number type is a string representation of a number. If you are simply wanting to output all numbers as strings, you don't need any custom types at all. Simply use the relevant string values in your code. For example:

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

This will always output a string as you are using the string type and not the json.Number type which has custom JSON marshal/unmarshal implementations.

答案2

得分: 0

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

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

谢谢你的帮助!

英文:

I have a solution which works for me. https://play.golang.org/p/Vr4eELsHs1

The keypoint was, that i have to take fmt.Sprint(*isbn) to return the string in the marshaler. I created a new type, convert the input to int64 with the json.Number function and convert it with the json custom marshaler to string.

Thank you for your help!

huangapple
  • 本文由 发表于 2017年8月23日 17:37:12
  • 转载请务必保留本文链接:https://go.coder-hub.com/45836035.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定