如何在Go中解析普通和带引号的JSON数字?

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

How to parse both plain and enquoted JSON numbers in Go?

问题

我正在处理一个基于JSON的第三方API。通常它会将所有数字用引号括起来,但有时候不会。我对此无能为力。

我正在尝试找到一个解决方案,无论数字是否被引号括起来,都可以解析它们。标准库提供了一个,string字段标签,允许将数字字段映射到带引号的值,但不幸的是,如果值没有引号,它就无法处理。

func test(s string) {
  err := json.Unmarshal([]byte(s), &struct {
    F1 float64
    F2 float64 `json:",string"`
  }{})
  if err != nil {
    fmt.Println(err)
    return
  }
  
  fmt.Println("success")
}

func main() {
  test(`{"f1": 1.23}`)   // success
  test(`{"f1": "1.23"}`) // 无法将字符串解组为类型为float64的Go值
  test(`{"f2": 1.23}`)   // 无效使用,string结构标签,尝试将未引用的值解组为...
  test(`{"f2": "1.23"}`) // success
}

这里有没有什么解决办法呢?

英文:

I'm dealing with a third-party JSON-based API. Usually it wraps all numbers in quotes, but sometimes it doesn't. Nothing I can do about it.

I'm trying to come up with a solution, which parses the numbers regardless of whether they're enquoted or not. Standard library provides a ,string field tag, which allows to map numeric fields to enquoted values, but, unfortunately, it then fails to process the value, if it's not in quotes.

<!-- language: go -->

func test(s string) {
  err := json.Unmarshal([]byte(s), &amp;struct {
    F1 float64
    F2 float64 `json:&quot;,string&quot;`
  }{})
  if err != nil {
    fmt.Println(err)
    return
  }
  
  fmt.Println(&quot;success&quot;)
}

func main() {
  test(`{&quot;f1&quot;: 1.23}`)   // success
  test(`{&quot;f1&quot;: &quot;1.23&quot;}`) // cannot unmarshal string into Go value of type float64
  test(`{&quot;f2&quot;: 1.23}`)   // invalid use of ,string struct tag, trying to unmarshal unquoted value into ...
  test(`{&quot;f2&quot;: &quot;1.23&quot;}`) // success
}

The Go Playground

Is there a way around this?

答案1

得分: 6

正确的解决方案是“克隆”float64并为其定义自定义的MarshalJSONUnmarshalJSON

type jsonFloat64 float64

func (f jsonFloat64) MarshalJSON() ([]byte, error) {
  return json.Marshal(float64(f))
}

func (f *jsonFloat64) UnmarshalJSON(data []byte) error {
  if len(data) >= 2 && data[0] == '"' && data[len(data)-1] == '"' {
    data = data[1 : len(data)-1]
  }

  var tmp float64
  err := json.Unmarshal(data, &tmp)
  if err != nil {
    return err
  }

  *f = jsonFloat64(tmp)
  return nil
}

然后你可以像这样使用它:

func test(s string) {
  err := json.Unmarshal([]byte(s), &struct {
    F jsonFloat64
  }{})
  if err != nil {
    fmt.Println(err)
    return
  }

  fmt.Println("success")
}

func main() {
  test(`{"f": 1.23}`)   // success
  test(`{"f": "1.23"}`) // success
}

Go Playground

请随意根据你的需求调整UnmarshalJSON,我的代码对空格要求比较严格。感谢Dave C,这个解决方案受到了他在另一个问题的评论的启发(该评论还提供了更多变体)。

另外,你也可以使用正则表达式预处理JSON数据,但如果上面的解决方案可行的话,不要这样做,因为它更快。

re := regexp.MustCompile(`(":\s*)([\d\.]+)(\s*[,}])`)
rawJsonByteArray = re.ReplaceAll(rawJsonByteArray, []byte(`$1"$2"$3`))
英文:

The proper solution is to "clone" float64 and define custom MarshalJSON and UnmarshalJSON for it:

<!-- language: golang -->

type jsonFloat64 float64

func (f jsonFloat64) MarshalJSON() ([]byte, error) {
  return json.Marshal(float64(f))
}

func (f *jsonFloat64) UnmarshalJSON(data []byte) error {
  if len(data) &gt;= 2 &amp;&amp; data[0] == &#39;&quot;&#39; &amp;&amp; data[len(data)-1] == &#39;&quot;&#39; {
    data = data[1 : len(data)-1]
  }

  var tmp float64
  err := json.Unmarshal(data, &amp;tmp)
  if err != nil {
    return err
  }

  *f = jsonFloat64(tmp)
  return nil
}

Then you'd be able to do something like this:

<!-- language: golang -->

func test(s string) {
  err := json.Unmarshal([]byte(s), &amp;struct {
    F jsonFloat64
  }{})
  if err != nil {
    fmt.Println(err)
    return
  }
  
  fmt.Println(&quot;success&quot;)
}

func main() {
  test(`{&quot;f&quot;: 1.23}`)   // success
  test(`{&quot;f&quot;: &quot;1.23&quot;}`) // success
}

The Go Playground

Feel free to adjust UnmarshalJSON to your needs, mine is pretty strict about the spacing. Credit goes to Dave C, this was heavily inspired by his comment on another question (which also features more variations on the solution above).

Alternatively, you could pre-process the JSON data with a regular expression, but don't do this if the solution above is a viable option, it's much faster.

<!-- language: golang -->

re := regexp.MustCompile(`(&quot;:\s*)([\d\.]+)(\s*[,}])`)
rawJsonByteArray = re.ReplaceAll(rawJsonByteArray, []byte(`$1&quot;$2&quot;$3`))

huangapple
  • 本文由 发表于 2015年7月25日 18:30:46
  • 转载请务必保留本文链接:https://go.coder-hub.com/31625511.html
匿名

发表评论

匿名网友

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

确定