Search code examples
iosswiftmulti-leveldecodablejsondecoder

Decoding multi-level JSON into structs in iOS with Swift 4


I'm trying to decode my data into structs. Here is an example of one of my data structure as JSON:

{
    "name": "abc",
    "owner": "bcd",
    "fallbackLanguage": "tr",
    "localizedValues": {
        "en": {
            "description": "Lorem Ipsum Dolor Sit Amet"
        },
        "de": {
            "description": "Sed Do Eiusmod Tempor Incididunt"
        },
        "tr": {
            "description": "Consectetur Adipisicing Elit"
        }
    }
}

And struct for this JSON object is:

struct X {
  let name: String
  let owner: String
  let fallbackLanguage: String

  let description: String
}

Decoding name, owner and fallbackLanguage is not a problem and already done. Here is the current CodingKey and init(from:)

struct CodingKeys: CodingKey {
    var stringValue: String
    init?(stringValue: String) {
      self.stringValue = stringValue
    }

    var intValue: Int?
    init?(intValue: Int) {
      self.intValue = intValue
      self.stringValue = "\(intValue)"

    }

    static func makeKey(name: String) -> CodingKeys {
      return CodingKeys.init(stringValue: name)!
    }
}

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)

    owner = try container.decode(String.self, forKey: CodingKeys.makeKey(name: "owner"))
    name = try container.decode(String.self, forKey: CodingKeys.makeKey(name: "name"))
    fallbackLanguage = try container.decode(String.self, forKey: CodingKeys.makeKey(name: "fallbackLanguage"))

    // FIXME: decode localizedValues into `description`
}

The problem is decoding description, since it's hold in a multi-level dictionary, and it's value will change due to device locale.

In this example, if device locale is not en, de or tr, it will fallback to tr since fallbackLanguage is tr.

Any help and suggestions would be great. Thank you.

Note: I included this gist by inamiy to encode / decode dictionaries and arrays.


Solution

  • Thanks to @AndyObusek, I've learned that there are nestedContainers.

    Here is the final solution for my problem:

        // get localizedValues as nested container
        let localizedValues = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.makeKey(name: "localizedValues")!)
    
        var localizations: KeyedDecodingContainer<CodingKeys>
    
        // if device locale identifier exists in the localizedValues container as a key get it as nested container, else get for fallbackLanguage.
    
        if localizedValues.contains(CodingKeysmakeKey(name: Locale.current.identifier)!) {
          localizations = try localizedValues.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.makeKey(name: Locale.current.identifier))
        } else {
          localizations = try localizedValues.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.makeKey(name: fallbackLanguage))
        }
    
        // set description
        description = try localizations.decode(String.self, forKey: CodingKeys.makeKey(name: "description"))
    

    EDIT:

    My data is a Cloud Firestore data, and I've found a great POD created by alickbass for encoding and decoding data for/from Firebase named CodableFirebase which is the simplest and best solution around if you use Firebase Database or Cloud Firestore