Search code examples
jsonswiftdecoderdecodable

Decoding JSON Response with same envelope but different content


I am accessing a web api and as response it sends different payloads wrapped in same envelope like that:

Get a list of recipes:

{
    "status": "SUCCESS",
    "messages": [],
    "input": null,
    "output": [
        {
            "id": 1,
            "title": "Egg with bacon"
        },
        {
            "id": 2,
            "title": "Ice cream"
        }
    ]
}

Get a single recipe:

{
    "status": "SUCCESS",
    "messages": [],
    "input": {"id": 1},
    "output": {
        "id": 1,
        "title": "Egg with bacon"
    }
}

An error response:

{
    "status": "ERROR",
    "messages": ["Recipe not found"],
    "input": {"id": 4},
    "output": null
}

List of categories:

{
    "status": "SUCCESS",
    "messages": [],
    "input": null,
    "output": [
        {
            "id": 1,
            "title": "Deserts"
        },
        {
            "id": 2,
            "title": "Main Courses"
        }
    ]
}

So, envelope keys are always present. Input is a key-value object or null, messages is always an array of strings or empty array, status is string. But output can be different. It can be a type of Recipe struct, array of Recipe structs or a Category struct.

My question is: How can i decode this json without writing every time the same decoding logic for envelope? I want to write a decoder just once for envelope and inject different decoders for output.


Solution

  • You can create a decodable struct that you will use to wrap your input and output into.

    This will look like:

    struct ResponseContainer<Input: Decodable, Output: Decodable>: Decodable {
        var status: String
        var messages: [String]
        var input: Input?
        var output: Output
    }
    

    Using this, if you want to decode a single Recipe, you just wrap your Recipe struct using into the response container:

    // used to decode the `input`
    struct InputId: Decodable {
        var id: Int
    }
    
    // content of the `output`
    struct Recipe: Decodable {
        var id: Int
        var title: String
    }
    
    try? JSONDecoder().decode(ResponseContainer<InputId, Recipe>.self, from: singleRecipeJson)
    

    If you want to decode a list of recipes, just go the same way with another struct or an array:

    // As the input is expected to be null, you can use a dummy struct in the wrapper.
    struct Empty: Decodable {}
    
    try! JSONDecoder().decode(ResponseContainer<Empty, [Recipe]>.self, from: multipleRecipeJson)
    

    Note: The dummy struct Empty might not be useful as it add a lot of complexity, and is used to parse the input property of the payload, which look like something you sent to the API (so basically you already know it, and it can be ignored). In that case, the wrapper would look like that:

    struct ResponseContainer<Output: Decodable>: Decodable {
        var status: String
        var messages: [String]
        var output: Output
    }