1

I have the follow example code:

type Num struct {
    X uint64 `json:"x,string"`
    Y float64 `json:"y,string"`
}

Now, if I run the code

js := []byte(`{"x": "123", "y": "1.23"}`)
var n Num
err := json.Unmarshal(js, &n)

it will parse ok.

But if I change the JSON to

js := []byte(`{"x": 123, "y": 1.23}`)

it returns an error.

The result I can understand.

Now, my problem is how can I make it accept both string and uint64/float64?

6
  • Tag field as float64, then convert what you want? Commented Aug 20, 2018 at 4:12
  • 2
    golang.org/pkg/encoding/json/#Number Commented Aug 20, 2018 at 5:14
  • You should give some more information about the api. Why do you have to deal with different types of x and y? How big is the JSON you will get? Commented Aug 20, 2018 at 5:35
  • If your json is small and the marshaling can be a little bit slower you could parse the input and replace "y": 1.23 with "y": "1.23" then you would always handle a string internal, but your api can handle both. Commented Aug 20, 2018 at 5:45
  • As @apxp if yor api is small change the type to string and then convert afterwards if required. But if the data is big or is not in your hand to change use interface{} Commented Aug 20, 2018 at 5:50

2 Answers 2

2

You will need to define a custom type that implements the json.Unmarshaler interface in such a way that the value can be either a string or a plain number.

The example below shows how to do it for a single type (uint64); you will need to repeat the pattern for any other numerical types (Go Playground):

type Uint64 uint64

type Num struct {
  X Uint64 `json:"x"`
}

func (u *Uint64) UnmarshalJSON(bs []byte) error {
  str := string(bs) // Parse plain numbers directly.
  if bs[0] == '"' && bs[len(bs)-1] == '"' {
    // Unwrap the quotes from string numbers.
    str = string(bs[1 : len(bs)-1])
  }
  x, err := strconv.ParseUint(str, 10, 64)
  if err != nil {
    return err
  }
  *u = Uint64(x)
  return nil
}

func main() {
  ss := []string{`{"x":"123"}`, `{"x":123}`}
  var n Num

  for _, s := range ss {
    err := json.Unmarshal([]byte(s), &n)
    fmt.Printf("OK: s=%-11s  n=%#v  err=%v\n", s, n, err)
  }
  // OK: s={"x":"123"}  n=main.Num{X:0x7b}  err=<nil>
  // OK: s={"x":123}    n=main.Num{X:0x7b}  err=<nil>
}
Sign up to request clarification or add additional context in comments.

Comments

1

Building on @maerics answer, you can defer both cases to the usual json unmarshaler, which feels a bit more robust:

package main

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

type Uint64 uint64

type Num struct {
    X Uint64 `json:"x"`
}

func (u *Uint64) UnmarshalJSON(bs []byte) error {
    var i uint64
    if err := json.Unmarshal(bs, &i); err == nil {
        *u = Uint64(i)
        return nil
    }
    var s string
    if err := json.Unmarshal(bs, &s); err != nil {
        return errors.New("expected a string or an integer")
    }
    if err := json.Unmarshal([]byte(s), &i); err != nil {
        return err
    }
    *u = Uint64(i)
    return nil
}

func main() {
    ss := []string{`{"x":"123"}`, `{"x":123}`, `{"x":0.12}`}
    var n Num

    for _, s := range ss {
        err := json.Unmarshal([]byte(s), &n)
        fmt.Printf("OK: s=%-11s  n=%#v  err=%v\n", s, n, err)
    }
}

which gives

OK: s={"x":"123"}  n=main.Num{X:0x7b}  err=<nil>
OK: s={"x":123}    n=main.Num{X:0x7b}  err=<nil>
OK: s={"x":0.12}   n=main.Num{X:0x7b}  err=expected a string or an integer

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.