Search code examples
jsongounmarshalling

Incorrect JSON is unmarshlled to struct without error


I have a case where server will respond with json error object when request failed for some reason, the server always responds with HTTP 200. So in case when my token expired and I request user info eg.:

type Person struct { FirstName string LastName string }

Instead getting {"FirstName": "Bob", "LastName": "Smith"} I am provided with {"error":401, "msg":"Unauthorized"}

I have a function that takes interface{} for unmarshalling:

func (ah *APIHandler) getObjectFromJson(bodyResponse string, target interface{}) *ServerError {
    parsingError := json.NewDecoder(strings.NewReader(bodyResponse)).Decode(target)
    // when server responds with ServerError I expect to get persingError here and proceed to unmarshalling the error message
    if parsingError != nil {
        fmt.Println(parsingError.Error())
        var err *ServerError = &ServerError{}
        parsingError = json.NewDecoder(strings.NewReader(bodyResponse)).Decode(err)
        if parsingError != nil {
            // this means unmarshalling ServerError failed
            panic(parsingError.Error())
        }
        return err
    }
    return nil
}

Putting it into a working example, when I provide incorrect JSON to passed interface{} I would expect to get error in the console "JSON doesn't match struct", not empty struct. Is that possible?

I have over 50 models, so ideally I would like to avoid writing unmarshaller for each of them to check if fields have been correctly unmarshalled, also I would like to avoid writing if strings.Contains(responseBody, "error") as some of the objects might contains string error in them.

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


Solution

  • The standard library always ignores unmapped fields when decoding. This is what you want, because otherwise you couldn't add new fields to your models without breaking every consumer of these models.

    Instead of checking for unrecognized fields, check if the response contains the error field, either by unmarshaling twice, or by parsing the error field in addition to the data you expect. You already do this, but you got it the other way around.

    func (ah *APIHandler) getObjectFromJson(bodyResponse string, target interface{}) *ServerError {
        b := []byte(bodyResponse)
    
        se := &ServerError{}
        if err := json.Unmarshal(b, &se); err != nil {
                // ...
        }
        if se.Error != "" {
                return se
        }
    
        if err := json.Unmarshal(b, target); err != nil {
                // ...
        }
    
        return nil
    }