英文:
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 (
"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)
}
}
}
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 >= '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
}
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:"id"`
Revision int `json:"revision"`
ISBN isbn `json:"isbn"`
Title string `json:"title"`
}
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:"id"`
// all other fields...
}
// Creates the public book representation from a book
func Public(b *Book) *PublicBook {
return &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!
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论