Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift 4 JSON Codable - value returned is sometimes an object, others an array

the data I'm getting from an API returns a single object but when there's multiple objects, it returns an array in the same key. With the current model (struct) I'm working with, the decoding fails when an array shows up.

These results are randomly ordered, meaning I can't know when I will be served an object or array.


Is there a way to create a model that takes this fact into account and can assign the correct type to cast for the value ('String' or '[String]') so that the decoding continues without problem?


This is an example of when an object is returned:

{
    "firstFloor": {
        "room": "Single Bed"
    }
}

This is an example of when an array is returned (for the same key):

{
    "firstFloor": {
        "room": ["Double Bed", "Coffee Machine", "TV", "Tub"]
    }
}

Example of the struct that should be able to be used as model to decode both samples above:

struct Hotel: Codable {
    let firstFloor: Room

    struct Room: Codable {
        var room: String // the type has to change to either array '[String]' or object 'String' depending on the returned results
    }
}

These results are randomly ordered, meaning I can't know when I will be served an object or array.

Here is the complete playground file:

import Foundation

// JSON with a single object
let jsonObject = """
{
    "firstFloor": {
        "room": "Single Bed"
    }
}
""".data(using: .utf8)!

// JSON with an array instead of a single object
let jsonArray = """
{
    "firstFloor": {
        "room": ["Double Bed", "Coffee Machine", "TV", "Tub"]
    }
}
""".data(using: .utf8)!

// Models
struct Hotel: Codable {
    let firstFloor: Room

    struct Room: Codable {
        var room: String // the type has to change to either array '[String]' or object 'String' depending on the results of the API
    }
}

// Decoding
let decoder = JSONDecoder()
let hotel = try decoder.decode(Hotel.self, from: jsonObject) //

print(hotel)
like image 589
Andres G Avatar asked Feb 12 '18 04:02

Andres G


1 Answers

You might encapsulate the ambiguity of the result using an Enum with Associated Values (String and Array in this case), for example:

enum MetadataType: Codable {
    case array([String])
    case string(String)

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        do {
            self = try .array(container.decode(Array.self))
        } catch DecodingError.typeMismatch {
            do {
                self = try .string(container.decode(String.self))
            } catch DecodingError.typeMismatch {
                throw DecodingError.typeMismatch(MetadataType.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Encoded payload not of an expected type"))
            }
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .array(let array):
            try container.encode(array)
        case .string(let string):
            try container.encode(string)
        }
    }
}

struct Hotel: Codable {
    let firstFloor: Room

    struct Room: Codable {
        var room: MetadataType
    }
}
like image 137
Andrea Mugnaini Avatar answered Oct 03 '22 05:10

Andrea Mugnaini