Said-Abdulla Atkaev
Said-Abdulla Atkaev

Reputation: 4323

Decode json with dynamic coding key using Codable?

Example of Json I need to decode. In "text" key we have [String: String] dict. And quantity of elements is in "count". How should I decode it properly?

{
"name": "LoremIpsum",
"index": "1",
"text": {
    "text_1": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ",
    "text_2": "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ",
    "text_3": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ",
    "text_4": "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
},
"count": "4"
}

My Codable model:

class Text: Codable {

private enum CodingKeys: String, CodingKey {
    case name, index, count, text
}

public var name: String?
public var index: Int?
public var count: Int?
public var texts: [String]?

init() {
    name = ""
    index = 0
    count = 0
    texts = []
}

init(name: String,
     index: Int,
     count: Int,
     texts: [String]) {
    self.name = name
    self.index = index
    self.count = count
    self.texts = texts
}

func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    var text = container.nestedContainer(keyedBy: CodingKeys.self, forKey: . text)

}    <---- also why do I need this method? 

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

    self.name = try container.decode(String.self, forKey: .name)
    self.index = Int(try container.decode(String.self, forKey: .index)) ?? 0
    self.count = Int(try container.decode(String.self, forKey: .count)) ?? 0
    let text = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .text)

    for i in (1...self.count!) {
        self.texts!.append(try text.decode(String.self, forKey: Text.CodingKeys.init(rawValue: "text_\(i)") ?? .text))
    }
}

}

And I decode it with:

if let path = Bundle.main.path(forResource: "en_translation_001", ofType: "json") {
        do {
            let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped)
            let jsonObj = try JSONDecoder().decode(Text.self, from: data)
            print("jsonData:\(jsonObj)")
        } catch let error {
            print("parse error: \(error.localizedDescription)")
        }
    } else {
        print("Invalid filename/path.")
    }

But I got parse error

parse error: The data couldn’t be read because it is missing.

What is wrong with my code? Is it a good way to decode such dynamic coding keys?

Upvotes: 0

Views: 1933

Answers (3)

Rob Napier
Rob Napier

Reputation: 299275

The most important thing you need is this:

let text = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .text)
texts = try text.allKeys.map { try text.decode(String.self, forKey: $0) }

This will convert the dictionary into an array of values. While in principle JSON values are not ordered, allKeys is ordered (because it's a serialized protocol).

You wouldn't need the encode method if you conformed only to Decodable rather than Codable. Also, most of your optionals are not really optionals.

Putting those things together, you would have this:

class Text: Decodable {

    private enum CodingKeys: String, CodingKey {
        case name, index, count, text
    }

    public var name: String
    public var index: Int
    public var count: Int
    public var texts: [String]

    init(name: String = "",
         index: Int = 0,
         count: Int = 0,
         texts: [String] = []) {
        self.name = name
        self.index = index
        self.count = count
        self.texts = texts
    }

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

        self.name = try container.decode(String.self, forKey: .name)
        self.index = Int(try container.decode(String.self, forKey: .index)) ?? 0
        self.count = Int(try container.decode(String.self, forKey: .count)) ?? 0
        let text = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .text)
        texts = try text.allKeys.map { try text.decode(String.self, forKey: $0) }
    }

}

let jsonObj = try JSONDecoder().decode(Text.self, from: data)

Upvotes: 2

Robert Dresler
Robert Dresler

Reputation: 11140

Fix JSON

I suppose you want your index and count to be numbers. So replace this "index": "1" and this "count": "4" With this "index": 1 and this "count": 4

Class structure

With Codable protocol any of these CodingKeys and encode functions or required inits aren't neccessary. Also change texts property data format to [String: String]

So, replace your class like this:

class Text: Codable {

    public var name: String
    public var index: Int
    public var count: Int
    public var texts: [String: String]

    init(name: String, index: Int, count: Int, texts: [String: String]) {
        self.name = name
        self.index = index
        self.count = count
        self.texts = texts
    }

}

Decoding

For decoding your json use what you wrote above, this is correct

let object = try JSONDecoder().decode(Text.self, from: data)

Upvotes: 2

Shehata Gamal
Shehata Gamal

Reputation: 100503

You need

struct Root: Codable {
    let name, index,count: String
    let text: [String:String]
}

--

let res = try? JSONDecoder().decode(Root.self,from:jsonData)

Upvotes: 2

Related Questions