JSON unmarshal integer field into a string
Asked Answered
K

3

7

I am struggling with deserializing a integer into a string struct field. The struct field is a string and is expected to be assignable from users of my library. That's why I want it to be a string, since for the purpose of writing it to the database I actually don't care about the value inside. The users can supply text, but some just assign integers.

Consider this struct:

type Test struct {
  Foo string
}

Sometimes I end up with a JSON value that is valid but won't deserialize into the struct due to the Foo field being a integer instead of a string:

{ "foo": "1" } // works
{ "foo": 1 } // doesn't

json.Unmarshal will blow up with the following error: json: cannot unmarshal number into Go struct field test.Foo of type string

See the reproduction: https://play.golang.org/p/4Qau3umaVm

Now in every other JSON library (in other languages) I have worked in so far, if the target field is a string and you get a integer the deserializer will usually just wrap the int in a string and be done with it. Can this be achieved in Go?

Since I can't really control how the data comes in I need to make json.Unmarshal unsensitive to this - the other solution would be to define Foo as interface{} which needlessly complicates my code with type assertions etc..

Any ideas on how to do this? I basically need the inverse of json:",string"

Knitted answered 30/8, 2017 at 19:37 Comment(1)
You could implement the json.Unmarshaler interface.Rhone
R
7

To handle big structs you can use embedding.

Updated to not discard possibly previously set field values.

func (t *T) UnmarshalJSON(d []byte) error {
    type T2 T // create new type with same structure as T but without its method set!
    x := struct{
        T2 // embed
        Foo json.Number `json:"foo"`
    }{T2: T2(*t)} // don't forget this, if you do and 't' already has some fields set you would lose them

    if err := json.Unmarshal(d, &x); err != nil {
        return err
    }
    *t = T(x.T2)
    t.Foo = x.Foo.String()
    return nil
}

https://play.golang.org/p/BytXCeHMvt

Rhone answered 30/8, 2017 at 20:2 Comment(6)
Good example (but that's not an alias)Roots
I know it's not the 1.9 alias, but wasn't this called aliasing before?Rhone
Nope, it's never been called an alias because it creates a new type. Doesn't mean others haven't called it an "alias" in the past, but I've been doing my best to be pedantic on the matter for years, especially once the real alias idea was brought up ;) The only real aliases previously were byte and rune, for uint8 and int32 respectively.Roots
Ah yes, I think it's because of rune and friends that I was calling every type T2 T1 an alias. Anyway thanks for the clarification.Rhone
This is exactly what I was looking for.. Thanks so much! I'll have to read up on embedding.Knitted
Love the way you get around a recursive Unmarshal. It's a better hack than what I came up with trying to Unmarshal the same struct in the UnmarshalJSON implementation.Ens
R
4

You can customize how the data structure is Unmarshaler by implementing the json.Unmarshaler interface.

The simplest way to handle unknown types is to unnmarshal the JSON into an intermediate structure, and handle the type assertions and validation during deserialization:

type test struct {
    Foo string `json:"foo"`
}

func (t *test) UnmarshalJSON(d []byte) error {
    tmp := struct {
        Foo interface{} `json:"foo"`
    }{}

    if err := json.Unmarshal(d, &tmp); err != nil {
        return err
    }

    switch v := tmp.Foo.(type) {
    case float64:
        t.Foo = strconv.Itoa(int(v))
    case string:
        t.Foo = v
    default:
        return fmt.Errorf("invalid value for Foo: %v", v)
    }

    return nil
}

https://play.golang.org/p/t0eI4wCxdB

Roots answered 30/8, 2017 at 19:53 Comment(1)
Thanks - I also thought about that. But this is a really big struct.. Is there a way how I can implement the unmarshaler only for the problematic field?Knitted
M
0

I got a rather simple solution in case you don't know what type to expect.
You always end up with a string.

type String string

func (s *String) UnmarshalJSON(d []byte) error {
    d = bytes.Trim(d, `"`) // get rid of string quotes
    *s = String(d)
    return nil
}

Working example: https://go.dev/play/p/XFLgtx6s4N1

Mortgagee answered 14/3 at 12:3 Comment(2)
Edge condition: a string that is quoted, e.g. "\"edge\"" by all expectations could be value \"edge\", but Trim removes the " from the second \" because it's near the end of the string.Moderator
bytes.Trim hides some internal functions like bytes.trimLeftByte that would be useful here, so we can just apply a similar logic, checking the left byte and right byte and removing if they are quotes (and making sure not to check indices that aren't in the slice): go.dev/play/p/UIAsipfLJTDModerator

© 2022 - 2024 — McMap. All rights reserved.