如何解析一个不一致的JSON字段,该字段既可以是字符串,也可以是字符串数组?

7

我在处理一些我无法控制的Json数据时遇到了解析问题。其中有一个字段99%的情况下是字符串,但偶尔会是一个数组。

type MyListItem struct {
    Date  string `json:"date"`
    DisplayName       string `json:"display_name"`
}

type MyListings struct {
    CLItems []MyListItem `json:"myitems"`
}

var mylist MyListings
err = json.Unmarshal(jsn, &mylist)
if err != nil {
    fmt.Print("JSON:\n%s\n error:%v\n", string(jsn),err)
    return
}

以下是Json:

{       
    "date": "30 Apr",
    "display_name": "Mr Smith"
},
{
    "date": "30 Apr",
    "display_name": ["Mr Smith", "Mr Jones"],
}

错误:json:无法将数组解组为类型为字符串的MyListItem.display_name Go结构字段

你的最终 DisplayName 应该是什么类型? - zerkms
1
如果您无法控制JSON,则可能需要将其解析为接口:https://blog.golang.org/json-and-go(请查看解码任意数据下面的部分) - Nick
@zerkms 我将把任何DisplayName数组连接成一个由'&'字符连接的字符串。 - John F
2
可能是如何在Golang中解析/反序列化动态JSON的重复问题。 - Jonathan Hall
2个回答

10
使用 json.RawMessage 来捕获不确定的字段。
使用 json 中的“-”名称来隐藏 DisplayName 字段,以便解码器忽略它。应用程序将在解码顶层 JSON 后填充此字段。
type MyListItem struct {
    Date           string          `json:"date"`
    RawDisplayName json.RawMessage `json:"display_name"`
    DisplayName    []string        `json:"-"`
}

解析顶层JSON:

var li MyListItem
if err := json.Unmarshal(data, &li); err != nil {
    // handle error
}

根据原始数据的类型解析显示名称:
if len(li.RawDisplayName) > 0 {
    switch li.RawDisplayName[0] {
    case '"':
        if err := json.Unmarshal(li.RawDisplayName, &li.DisplayName); err != nil {
            // handle error
        }
    case '[':
        var s []string
        if err := json.Unmarshal(li.RawDisplayName, &s); err != nil {
            // handle error
        }
        // Join arrays with "&" per OP's comment on the question.
        li.DisplayName = strings.Join(s, "&")
    }
}

playground 示例

将上述内容合并到for循环中以处理MyListings

var listings MyListings
if err := json.Unmarshal([]byte(data), &listings); err != nil {
    // handle error
}
for i := range listings.CLItems {
    li := &listings.CLItems[i]
    if len(li.RawDisplayName) > 0 {
        switch li.RawDisplayName[0] {
        case '"':
            if err := json.Unmarshal(li.RawDisplayName, &li.DisplayName); err != nil {
                // handle error
            }
        case '[':
            var s []string
            if err := json.Unmarshal(li.RawDisplayName, &s); err != nil {
                // handle error
            }
            li.DisplayName = strings.Join(s, "&")
        }
    }
}

示例

如果数据模型中有一个值可以是字符串或字符串切片,则将逻辑封装在类型中会很有帮助。在实现json.Unmarshaler接口时解析JSON数据。

type multiString string

func (ms *multiString) UnmarshalJSON(data []byte) error {
    if len(data) > 0 {
        switch data[0] {
        case '"':
            var s string
            if err := json.Unmarshal(data, &s); err != nil {
                return err
            }
            *ms = multiString(s)
        case '[':
            var s []string
            if err := json.Unmarshal(data, &s); err != nil {
                return err
            }
            *ms = multiString(strings.Join(s, "&"))
        }
    }
    return nil
}

使用方法如下:

type MyListItem struct {
    Date        string      `json:"date"`
    DisplayName multiString `json:"display_name"`
}

type MyListings struct {
    CLItems []MyListItem `json:"myitems"`
}

var listings MyListings
if err := json.Unmarshal([]byte(data), &listings); err != nil {
    log.Fatal(err)
}

示例程序

以下代码可以将值作为字符串切片获取,而不是使用 & 连接的单个字符串。

type multiString []string

func (ms *multiString) UnmarshalJSON(data []byte) error {
    if len(data) > 0 {
        switch data[0] {
        case '"':
            var s string
            if err := json.Unmarshal(data, &s); err != nil {
                return err
            }
            *ms = multiString{s}
        case '[':
            if err := json.Unmarshal(data, (*[]string)(ms)); err != nil {
                return err
            }
        }
    }
    return nil
}

Playground example.


谢谢@ThunderCat,这个方法很管用。了解json.Rawmessage非常有用。 - John F
你可以通过让MyListItem提供UnmarshalJSON的实现来使其更加自包含:https://play.golang.org/p/vXFwQBhYQvR - laz

6
作为一种替代方法,这个方案基于@ThunderCat的答案,但不是使用json.RawMessage,而是使用interface{}和类型切换来实现:
package main

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

type MyListItem struct {
    Date           string      `json:"date"`
    DisplayName    string      `json:"-"`
    RawDisplayName interface{} `json:"display_name"`
}

func (li *MyListItem) UnmarshalJSON(data []byte) error {
    type localItem MyListItem
    var loc localItem
    if err := json.Unmarshal(data, &loc); err != nil {
        return err
    }
    *li = MyListItem(loc)
    switch li.RawDisplayName.(type) {
    case string:
        li.DisplayName = li.RawDisplayName.(string)
    case []interface{}:
        vals := li.RawDisplayName.([]interface{})
        if len(vals) > 0 {
            li.DisplayName, _ = vals[0].(string)
            for _, v := range vals[1:] {
                li.DisplayName += "&" + v.(string)
            }
        }
    }
    return nil
}

func test(data string) {
    var li MyListItem
    if err := json.Unmarshal([]byte(data), &li); err != nil {
        log.Fatal(err)
    }
    fmt.Println(li.DisplayName)
}

func main() {
    test(`
{       
    "date": "30 Apr",
    "display_name": "Mr Smith"
}`)

    test(`
{
    "date": "30 Apr",
    "display_name": ["Mr Smith", "Mr Jones"]
}`)

}

在线代码运行示例


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