Search code examples
iosjsonswiftnsobjectcodable

Decoding JSON into Codable Objects - with condition


I would like to decode JSON into Objects with Codable protocol.

The outcome that I want to achieve is:

[
 [ Collection
    < collectionType = item
    < collectionName = some name`
    < data = [ Item
                 < itemTitle = title
                 < itemSubtitle = subtitle,
               Item
                 < itemTitle = title
                 < itemSubtitle = subtitle ],
[ Collection
    < collectionType = location
    < collectionName = some name`
    < data = [ Location
                 < locationName = someName,
               Location
                 < locationName = someName ],
[ Collection
    < collectionType = item
    < collectionName = some name`
    < data = [ Item
                 < itemTitle = title
                 < itemSubtitle = subtitle,
               Item
                 < itemTitle = title
                 < itemSubtitle = subtitle ],
[ Collection
    < collectionType = location
    < collectionName = some name`
    < data = [ Location
                 < locationName = someName,
               Location
                 < locationName = someName ]]

The JSON is as follows:

    [{
        "collectionType": "item",
        "collectionName": "some name",
        "data": [
            {
                "itemTitle": "title",
                "itemSubtitle": "subtitle",
            },
            {
                "itemTitle": "title",
                "itemSubtitle": "subtitle",
            }
         ]
      },
      {
        "collectionType": "location",
        "collectionName": "some name",
        "data": [
            {
                "locationName": "a name",
            },
            {
                "locationName": "a name",
            }
         ]
      },
      {
        "collectionType": "item",
        "collectionName": "some name",
        "data": [
            {
                "itemTitle": "title",
                "itemSubtitle": "subtitle",
            },
            {
                "itemTitle": "title",
                "itemSubtitle": "subtitle",
            }
         ]
      },
      {
        "collectionType": "location",
        "collectionName": "some name",
        "data": [
            {
                "locationName": "a name",
            },
            {
                "locationName": "a name",
            }
         ]
      }
  ]

As you can see the Collection will be of type item or location. And the data will be according to that type. How should I achieve that with Codable?

My objects are as follows:

class Collection: NSObject, Codable {

    // MARK: - Properties

    let collectionType: String
    let collectionName: String
    let data????

    // MARK: - Keyes

    private enum CodingKeys: String, CodingKey {
        case collectionType
        case collectionName
    }
}

class Item: NSObject, Codable {

    // MARK: - Properties

    let itemTitle: String
    let itemSubtitle: String

    // MARK: - Keyes

    private enum CodingKeys: String, CodingKey {
        case itemTitle
        case itemSubtitle
    }
}

class Location: NSObject, Codable {

    // MARK: - Properties

    let locationName: String

    // MARK: - Keyes

    private enum CodingKeys: String, CodingKey {
        case locationName
    }
}

How can I propagate data with the appropriate objects?


Solution

  • I suggest two approaches:

    Approach 1

    Change your data structure to remove the ambiguity of whether data describes an item or a location:

    [{
        "collectionName": "some name",
        "items": [
            {
                "itemTitle": "title",
                "itemSubtitle": "subtitle",
            },
            {
                "itemTitle": "title",
                "itemSubtitle": "subtitle",
            }
        ]
    },
    {
        "collectionName": "some name",
        "locations": [
            {
                "locationName": "a name",
            },
            {
                "locationName": "another name",
            }
        ]
    }]
    

    ... and modify your Collection to have an optional locations and optional items.

    Approach 2

    If changing your JSON structure is not an option, then I suggest changing your Collection class to:

    class Collection: Codable {
        let collectionType: String
        let collectionName: String
        let data: [CollectionData]
    }
    

    ... and creating an enum CollectionData:

    enum CollectionError: Error {
        case invalidData
    }
    
    enum CollectionData {
        case item(Item)
        case location(Location)
    }
    
    extension CollectionData: Codable {
        init(from decoder: Decoder) throws {
            if let item = try? Item(from: decoder) {
                self = .item(item)
                return
            }
    
            if let location = try? Location(from: decoder) {
                self = .location(location)
                return
            }
    
            throw CollectionError.invalidData
        }
    
        func encode(to encoder: Encoder) throws {
            switch self {
            case .item(let item):
                try item.encode(to: encoder)
            case .location(let location):
                try location.encode(to: encoder)
            }
        }
    }
    

    Pros and cons of the two approaches:

    Approach 1

    Pro: Makes the data more self-descriptive

    Con: Allows a collection with neither items nor locations

    Approach 2

    Pro: Works with existing data structure

    Con: Would allow a data array that was partly Location and partly Item

    Unless there's more to your real code, you seem to be defining CodingKeys exactly as the default would be, so you can probably remove that.