1

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.

Arda Oğul Üçpınar
  • 881
  • 1
  • 14
  • 38

5 Answers5

2

I recommend to create a struct for localizedValues and decode the value into a dictionary. Then get the current fallbackLanguage and get the corresponding description from the dictionary. Custom CodingKeys and initializer is not needed.

let jsonString = """
{    "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"}
    }
}
"""

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

    let localizedValues: [String:LocalizedValue]
}

struct LocalizedValue : Decodable {
    let description : String
}

let data = Data(jsonString.utf8)
do {
    let result = try JSONDecoder().decode(X.self, from: data)
    let fallBackLanguage = result.fallbackLanguage
    let fallBackLanguageDescription = result.localizedValues[fallBackLanguage]?.description
    print(fallBackLanguageDescription)

} catch { print(error)}
vadian
  • 274,689
  • 30
  • 353
  • 361
  • I've thought about that. But `localizedValues` are shared across my all structs and their content vary. One has only `description`, one has `name` too, one does not have `description` etc. So I decided not to do that. – Arda Oğul Üçpınar Apr 13 '18 at 11:35
2

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

Arda Oğul Üçpınar
  • 881
  • 1
  • 14
  • 38
2

You could use this extension for encoding and decoding Array<Any> and Dictionary<String: Any>

Swift 4:

struct X: Decodable {

    let name: String?
    let owner: String?
    let fallbackLanguage: String?
    let localizedValues: Dictionary<String, Any>?

    enum CodingKeys: String, CodingKey {
        case name, owner, fallbackLanguage, localizedValues
    }
}
Lloyd Keijzer
  • 1,229
  • 1
  • 12
  • 22
  • 1
    It's much like the extension that I use. Thank you. – Arda Oğul Üçpınar Apr 13 '18 at 21:04
  • @LLoyd Keijzer Whats name of extension? I see 404 on the link you mentioned https://github.com/3D4Medical/glTFSceneKit/blob/master/Sources/gltf_scenekit/GLTF/JSONCodingKeys.swift – Naren Feb 17 '20 at 04:23
  • @Naren You can find it here https://github.com/3D4Medical/glTFSceneKit/blob/master/Sources/glTFSceneKit/GLTF/JSONCodingKeys.swift – Lloyd Keijzer Feb 17 '20 at 06:12
0

With your json you can create following struct

struct LocalizedJSONObject: Codable {
    let name, owner, fallbackLanguage: String?
    let localizedValues: LocalizedValues?
}

struct LocalizedValues: Codable {
    let en, de, tr: De?
}

struct De: Codable {
    let description: String?
}

// MARK: Convenience initializers

extension LocalizedJSONObject {
    init(data: Data) throws {
        self = try JSONDecoder().decode(LocalizedJSONObject.self, from: data)
    }

    init(_ json: String, using encoding: String.Encoding = .utf8) throws {
        guard let data = json.data(using: encoding) else {
            throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
        }
        try self.init(data: data)
    }

    init(fromURL url: URL) throws {
        try self.init(data: try Data(contentsOf: url))
    }

    func jsonData() throws -> Data {
        return try JSONEncoder().encode(self)
    }

    func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
        return String(data: try self.jsonData(), encoding: encoding)
    }
}

extension LocalizedValues {
    init(data: Data) throws {
        self = try JSONDecoder().decode(LocalizedValues.self, from: data)
    }

    init(_ json: String, using encoding: String.Encoding = .utf8) throws {
        guard let data = json.data(using: encoding) else {
            throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
        }
        try self.init(data: data)
    }

    init(fromURL url: URL) throws {
        try self.init(data: try Data(contentsOf: url))
    }

    func jsonData() throws -> Data {
        return try JSONEncoder().encode(self)
    }

    func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
        return String(data: try self.jsonData(), encoding: encoding)
    }
}

extension De {
    init(data: Data) throws {
        self = try JSONDecoder().decode(De.self, from: data)
    }

    init(_ json: String, using encoding: String.Encoding = .utf8) throws {
        guard let data = json.data(using: encoding) else {
            throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
        }
        try self.init(data: data)
    }

    init(fromURL url: URL) throws {
        try self.init(data: try Data(contentsOf: url))
    }

    func jsonData() throws -> Data {
        return try JSONEncoder().encode(self)
    }

    func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
        return String(data: try self.jsonData(), encoding: encoding)
    }
}

And use like this

let localizedJSONObject = try LocalizedJSONObject(json)
Prashant Tukadiya
  • 15,838
  • 4
  • 62
  • 98
  • I've already implemented convenience initializers but did not add to this question, since they are irrelevant to the real question. And about the `LocalizedValues` struct, I've already thought about that. But localizedValues are shared across my all structs and their content vary. One has only description, one has name too, one does not have description etc. So I decided not to do that. – Arda Oğul Üçpınar Apr 13 '18 at 21:09
0

If you're not tight on a schedule, give this library a shot: Swifty Json

It has a few built in methods that might abstract your needs, such as casting a nested-dictionary Json object into an NSDictionary, where you can extract values out and apply control flow over them to build a logic around.

Mert Kahraman
  • 445
  • 6
  • 10
  • 1
    SwiftyJSON is a great library but as JSON parser it became obsolete after introducing native JSON decoding in Swift 4 and you are discouraged from using `NSDictionary` in Swift at all. – vadian Apr 13 '18 at 21:00
  • Thank you, Mert. I will take a look at that but I have to consider what @vadian said too. – Arda Oğul Üçpınar Apr 13 '18 at 21:06