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"

1
  • You could implement the json.Unmarshaler interface. Commented Aug 30, 2017 at 19:44

3 Answers 3

8

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

Sign up to request clarification or add additional context in comments.

6 Comments

Good example (but that's not an alias)
I know it's not the 1.9 alias, but wasn't this called aliasing before?
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.
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.
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.
|
5

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

1 Comment

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?
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

2 Comments

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.
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/UIAsipfLJTD

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.