2

I am working with a JSON response that can sometimes return a string or an object with string keys but values that are string and bool. I understand I need to implement my own Unmarshaler for the data

Example JSON Situations:

caseOne := `"data": [
    {"user": "usersName"}
]`

caseTwo := `"data": [
    {"user": {"id": "usersId", "isActive": true}}
]`

My Code:

package main

type Result struct {
    Data []Item `json:"data"`
}

type Item struct {
    User User `json:"user"`
}

type User struct {
    user string
}

func (u *User) MarshalJSON() ([]byte, error) {
    return json.Marshal(u.user)
}

func (u *User) UnmarshalJSON(data []byte) error {
    var raw interface{}
    json.Unmarshal(data, &raw)

    switch raw := raw.(type) {
    case string:
        *u = User{raw}
    case map[string]interface{}:
        // how do I handle the other "isActive" key that is map[string]bool?
        *u = User{raw["id"].(string)}
    }
    return nil

}

This question/answer: Here comes close to answering my use case but I am a bit stuck on how to handle multiple map values of different value types.

Current Go Playground: Here

4
  • 1
    Use type any to represent different map value types: map[string]any. Alternatively, define struct type with appropriate fields: struct { ID string `json:"id"`; IsActive bool `json:"isActive"` } Commented Apr 13, 2023 at 17:03
  • @CeriseLimón I'm not quite sure I follow how to take that into my example. I understand what you are stating, but how it ties into this I'm not quite sure I can piece it together. Here is my current playground: go.dev/play/p/7HfuEAAUdu5 Commented Apr 13, 2023 at 17:33
  • @CeriseLimón I've updated my playground to retrieve somewhat close of a result and added it to my question. Commented Apr 13, 2023 at 17:41
  • mkopriva's answer shows how to piece it together. Commented Apr 13, 2023 at 17:51

1 Answer 1

3
type User struct {
    Id       string `json:"id"`
    Name     string `json:"name"`
    IsActive bool   `json:"isActive"`
}

func (u User) MarshalJSON() ([]byte, error) {
    if u == (User{Name: u.Name}) { // check if u contains only name
        return json.Marshal(u.Name)
    }
    type U User
    return json.Marshal(U(u))
}

func (u *User) UnmarshalJSON(data []byte) error {
    switch data[0] {
    case '"': // string?
        return json.Unmarshal(data, &u.Name)
    case '{': // object?
        type U User
        return json.Unmarshal(data, (*U)(u))
    }

    return fmt.Errorf("unsupported JSON: %s", string(data))
}

https://go.dev/play/p/toOIz0XOQUo


If you pass u directly to json.Marshal from inside MarshalJSON, or if you pass it directly to json.Unmarshal from inside UnmarshalJSON, your program will get stuck in infinite recursion and eventually overflow the stack since MarshalJSON/UnmarshalJSON are called automatically by json.Marshal/json.Unmarshal on any value that implements those methods.

Type U is used to avoid this problem.

The statement type U User declares a new type U with its underlying type identical to that of User. Because the underlying types are identical we can easily convert one type to the other and back. However, the type declaration statement does not "carry over" the methods from the old type to the new type, so the new type U has none of the methods previously declared on User and therefore json.Marshal/json.Unmarshal will not get stuck in infinite call recursion anymore.

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

2 Comments

Thanks for this great write-up. Would the technique of creating a new Type be considered "type shadowing" or something else? Want to make sure I understand what this pattern is considered.
@Coldchain9 I'm afraid I don't know whether or not the pattern has a name.

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.