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

Coldchain9
  • 1,373
  • 11
  • 31
  • 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"\` }` – Charlie Tumahai Apr 13 '23 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: https://go.dev/play/p/7HfuEAAUdu5 – Coldchain9 Apr 13 '23 at 17:33
  • @CeriseLimón I've updated my playground to retrieve somewhat close of a result and added it to my question. – Coldchain9 Apr 13 '23 at 17:41
  • mkopriva's answer shows how to piece it together. – Charlie Tumahai Apr 13 '23 at 17:51

1 Answers1

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.

mkopriva
  • 35,176
  • 4
  • 57
  • 71
  • 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 Apr 14 '23 at 12:11
  • 1
    @Coldchain9 I'm afraid I don't know whether or not the pattern has a name. – mkopriva Apr 14 '23 at 12:36