Codable JSON parsing - Sometimes result is nil and flattened model problem


#1

Hi, I’m trying to flatten a model to decode a JSON result, but basically don’t know how to add an optional to a type that might be returned as nil.

Here is are different examples of returned data:

1- Array:

let jsonArray = """
{
    "room": {
        "media": {
            "photos": {
                "photo":
                    [
                        {"genericKey": "http://www.example.com/1"},
                        {"genericKey": "http://www.example.com/2"},
                        {"genericKey": "http://www.example.com/3"}
                    ]
                }
            }
        }
}
""".data(using: .utf8)!

2- Object:

let jsonObject = """
{
    "room": {
        "media": {
            "photos": {
                "photo": {"genericKey": "http://www.example.com/1"}
                }
            }
        }
}
""".data(using: .utf8)!

3- Nil:

let jsonNil = """
{
    "room": {
        "media": {}
    }
}
""".data(using: .utf8)!

Nested Model
Using the following model (nested), the decoding process succeeds because the photos property in the Media struct is optional.

// Model : Nested //
struct HotelNested: Codable {
    var room: Room
}

struct Room: Codable {
    var media: Media
}

struct Media: Codable {
    var photos: Photos?
}

struct Photos: Codable {
    var photo: ArrayOrObject
}

// 
struct GenericElement: Codable {
    var genericKey: String
}

Flat Model
When I try to flatten the model, this is where I don’t know where to indicate that the photos property is an optional. I don’t know if I should use an enum, or if the solution is simpler.

struct HotelFlat: Codable {
    var room: ArrayOrObject
    
    enum CodingKeys: String, CodingKey {
        case room
    }
    
    enum RoomCodingKeys: String, CodingKey {
        case media
    }
    
    enum MediaCodingKeys: String, CodingKey {
        case photos
    }
    
    enum PhotosCodingKeys: String, CodingKey {
        case photo
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let roomContainer = try container.nestedContainer(keyedBy: RoomCodingKeys.self, forKey: .room)
        let mediaContainer = try roomContainer.nestedContainer(keyedBy: MediaCodingKeys.self, forKey: .media)
        let photosContainer = try mediaContainer.nestedContainer(keyedBy: PhotosCodingKeys.self, forKey: .photos)
        
        room = try photosContainer.decode(ArrayOrObject.self, forKey: .photo)
    }
    
}

//////

Here is the complete playground file:

import Foundation

//result type : Array
let jsonArray = """
{
    "room": {
        "media": {
            "photos": {
                "photo":
                    [
                        {"genericKey": "http://www.example.com/1"},
                        {"genericKey": "http://www.example.com/2"},
                        {"genericKey": "http://www.example.com/3"}
                    ]
                }
            }
        }
}
""".data(using: .utf8)!

// result type : Object
let jsonObject = """
{
    "room": {
        "media": {
            "photos": {
                "photo": {"genericKey": "http://www.example.com/1"}
                }
            }
        }
}
""".data(using: .utf8)!

// result type : Empty/Nil
let jsonNil = """
{
    "room": {
        "media": {}
    }
}
""".data(using: .utf8)!


// Model : Nested //
struct HotelNested: Codable {
    var room: Room
}

struct Room: Codable {
    var media: Media
}

struct Media: Codable {
    var photos: Photos?
}

struct Photos: Codable {
    var photo: ArrayOrObject
}

//
struct GenericElement: Codable {
    var genericKey: String
}

// Model : Flat //
struct HotelFlat: Codable {
    var room: ArrayOrObject
    
    enum CodingKeys: String, CodingKey {
        case room
    }
    
    enum RoomCodingKeys: String, CodingKey {
        case media
    }
    
    enum MediaCodingKeys: String, CodingKey {
        case photos
    }
    
    enum PhotosCodingKeys: String, CodingKey {
        case photo
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let roomContainer = try container.nestedContainer(keyedBy: RoomCodingKeys.self, forKey: .room)
        let mediaContainer = try roomContainer.nestedContainer(keyedBy: MediaCodingKeys.self, forKey: .media)
        let photosContainer = try mediaContainer.nestedContainer(keyedBy: PhotosCodingKeys.self, forKey: .photos)
        
        room = try photosContainer.decode(ArrayOrObject.self, forKey: .photo)
    }
    
}

// Find if data is array or object and assign to model
enum ArrayOrObject: Codable {
    case array([GenericElement])
    case element(GenericElement)
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        
        do {
            self = try .array(container.decode(Array.self))
        } catch DecodingError.typeMismatch {
            do {
                self = try .element(container.decode(GenericElement.self))
            } catch DecodingError.typeMismatch {
                throw DecodingError.typeMismatch(ArrayOrObject.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Not of expected type."))
            }
        }
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .array(let array):
            try container.encode(array)
        case .element(let element):
            try container.encode(element)
        }
    }
}

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

dump(hotel)

#2

In the case of jsonNil, your document looks like this:

{
    "room": {
        "media": {}
    }
}

It’s missing the containing photos key as well as the inner photo key. So it’s failing on this line:

let photosContainer = try mediaContainer.nestedContainer(keyedBy: PhotosCodingKeys.self, forKey: .photos)

We can check to see if this key exists before trying to get the container for it:

if mediaContainer.contains(.photos) {
    let photosContainer = try mediaContainer.nestedContainer(keyedBy: PhotosCodingKeys.self, forKey: .photos)
    // ...
}

Next we need to handle the case where this container might not have a photo key either, so we need to make the property optional:

var room: ArrayOrObject?

Then inside that if statement above we only try to decode this object if the key is present in the container:

    room = try photosContainer.decodeIfPresent(ArrayOrObject.self, forKey: .photo)

Once you do this your type decodes properly in all 3 cases.

Hope this helps!