Search code examples
jsongomarshalling

Custom Marshaler for JSON that can be string or a map[string]string / map[string]bool


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


Solution

  • 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.