Using Decodable protocol with multiples keys
Asked Answered
F

6

1

Say I have the following code:

import Foundation

let jsonData = """
[
    {"firstname": "Tom", "lastname": "Smith", "age": {"realage": "28"}},
    {"firstname": "Bob", "lastname": "Smith", "age": {"fakeage": "31"}}
]
""".data(using: .utf8)!

struct Person: Codable {
    let firstName, lastName: String
    let age: String?

    enum CodingKeys : String, CodingKey {
        case firstName = "firstname"
        case lastName = "lastname"
        case age
    }
}

let decoded = try JSONDecoder().decode([Person].self, from: jsonData)
print(decoded)

Everything is working except age is always nil. Which makes sense. My question is how can I set the Person's age = realage or 28 in the first example, and nil in the second example. Instead of age being nil in both cases I want it to be 28 in the first case.

Is there a way to achieve this only using CodingKeys and not having to add another struct or class? If not how can I use another struct or class to achieve what I want in the simplest way possible?

Flashing answered 13/10, 2017 at 6:17 Comment(4)
You can write in it and encode yourself. In there you can ask for the nested container of age and set the value accordingly.Held
@Held So I just found this on Encode and Decode Manually and AdditionalInfoKeys. But am getting an error Designated initializer cannot be declared in an extension of 'Person'; did you mean this to be a convenience initializer? and Initializer requirement 'init(from:)' can only be satisfied by a 'required' initializer in the definition of non-final class 'Person'Flashing
Yeah, you can’t do it in an extension, you have to write it exactly where you declare the type.Held
for similar requirement you can use github.com/muhammadali2012/ModelBought
F
5

My favorite approach when it comes to decoding nested JSON data is to define a "raw" model that stays very close to the JSON, even using snake_case if needed. It help bringing JSON data into Swift really quickly, then you can use Swift to do the manipulations you need:

struct Person: Decodable {
    let firstName, lastName: String
    let age: String?

    // This matches the keys in the JSON so we don't have to write custom CodingKeys    
    private struct RawPerson: Decodable {
        struct RawAge: Decodable {
            let realage: String?
            let fakeage: String?
        }

        let firstname: String
        let lastname: String
        let age: RawAge
    }

    init(from decoder: Decoder) throws {
        let rawPerson  = try RawPerson(from: decoder)
        self.firstName = rawPerson.firstname
        self.lastName  = rawPerson.lastname
        self.age       = rawPerson.age.realage
    }
}

Also, I recommend you to be judicious with the use of Codable, as it implies both Encodable and Decodable. It seems like you only need Decodable so conform your model to that protocol only.

Faenza answered 14/10, 2017 at 1:23 Comment(0)
P
3

For greater flexibility and robustness, you could implement an Age enumeration to fully support your data model head-on ;) For instance:

enum Age: Decodable {
    case realAge(String)
    case fakeAge(String)

    private enum CodingKeys: String, CodingKey {
        case realAge = "realage", fakeAge = "fakeage"
    }

    init(from decoder: Decoder) throws {
        let dict = try decoder.container(keyedBy: CodingKeys.self)
        if let age = try dict.decodeIfPresent(String.self, forKey: .realAge) {
            self = .realAge(age)
            return
        }
        if let age = try dict.decodeIfPresent(String.self, forKey: .fakeAge) {
            self = .fakeAge(age)
            return
        }
        let errorContext = DecodingError.Context(
            codingPath: dict.codingPath,
            debugDescription: "Age decoding failed"
        )
        throw DecodingError.keyNotFound(CodingKeys.realAge, errorContext)
    }
}

and then use it in your Person type:

struct Person: Decodable {
    let firstName, lastName: String
    let age: Age

    enum CodingKeys: String, CodingKey {
        case firstName = "firstname"
        case lastName = "lastname"
        case age
    }

    var realAge: String? {
        switch age {
        case .realAge(let age): return age
        case .fakeAge: return nil
        }
    }
}

Decode as before:

let jsonData = """
[
    {"firstname": "Tom", "lastname": "Smith", "age": {"realage": "28"}},
    {"firstname": "Bob", "lastname": "Smith", "age": {"fakeage": "31"}}
]
""".data(using: .utf8)!

let decoded = try! JSONDecoder().decode([Person].self, from: jsonData)
for person in decoded { print(person) }

prints:

Person(firstName: "Tom", lastName: "Smith", age: Age.realAge("28"))
Person(firstName: "Bob", lastName: "Smith", age: Age.fakeAge("31"))


Finally, the new realAge computed property provides the behavior you were after initially (i.e., non-nil only for real ages):

for person in decoded { print(person.firstName, person.realAge) }

Tom Optional("28")
Bob nil

Pion answered 13/10, 2017 at 13:58 Comment(0)
A
3

There are times to trick the API to get the interface you want.

let jsonData = """
[
    {"firstname": "Tom", "lastname": "Smith", "age": {"realage": "28"}},
    {"firstname": "Bob", "lastname": "Smith", "age": {"fakeage": "31"}}
]
""".data(using: .utf8)!

struct Person: Codable {
    let firstName: String
    let lastName: String
    var age: String? { return _age["realage"] }

    enum CodingKeys: String, CodingKey {
        case firstName = "firstname"
        case lastName = "lastname"
        case _age = "age"
    }

    private let _age: [String: String]
}

do {
    let decoded = try JSONDecoder().decode([Person].self, from: jsonData)
    print(decoded)

    let encoded = try JSONEncoder().encode(decoded)
    if let encoded = String(data: encoded, encoding: .utf8) { print(encoded) }
} catch {
    print(error)
}

Here the API (firstName, lastName, age) is kept and the JSON is preserved in both directions.

Aversion answered 13/10, 2017 at 14:56 Comment(0)
K
2

You can use like this :

struct Person: Decodable {
    let firstName, lastName: String
    var age: Age?

    enum CodingKeys: String, CodingKey {
        case firstName = "firstname"
        case lastName = "lastname"
        case age
    }
}

struct Age: Decodable {
    let realage: String?
}

You can call like this :

do {
    let decoded = try JSONDecoder().decode([Person].self, from: jsonData)
    print(decoded[0].age?.realage) // Optional("28")
    print(decoded[1].age?.realage) // nil
} catch {
    print("error")
}
Kolb answered 13/10, 2017 at 19:10 Comment(0)
F
1

Lots of great answers here. I have certain reasons for not wanting to make it into it's own data model. Specially in my case it comes with a lot of data I don't need and this specific thing I need corresponds more to a person than an age model.

I'm sure others will find this post useful tho which is amazing. Just to add to that I will post my solution for how I decided to do this.

After looking at the Encoding and Decoding Custom Types Apple Documentation, I found it was possible to build a custom decoder and encoder to achieve this (Encode and Decode Manually).

struct Coordinate: Codable {
    var latitude: Double
    var longitude: Double
    var elevation: Double

    enum CodingKeys: String, CodingKey {
        case latitude
        case longitude
        case additionalInfo
    }

    enum AdditionalInfoKeys: String, CodingKey {
        case elevation
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        latitude = try values.decode(Double.self, forKey: .latitude)
        longitude = try values.decode(Double.self, forKey: .longitude)

        let additionalInfo = try values.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo)
        elevation = try additionalInfo.decode(Double.self, forKey: .elevation)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(latitude, forKey: .latitude)
        try container.encode(longitude, forKey: .longitude)

        var additionalInfo = container.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo)
        try additionalInfo.encode(elevation, forKey: .elevation)
    }

}

The one change that is included in the code above that Apple doesn't mention is the fact that you can't use extensions like in their documentation example. So you have to embed it right within the struct or class.

Hopefully this helps someone, along with the other amazing answers here.

Flashing answered 13/10, 2017 at 20:43 Comment(3)
...all these good answers just goes to show how powerful/flexible this Swift 4 encoding/decoding thing really is ;)Pion
@PauloMattos Totally, just wish there was more documentation and information for some of it haha. So hopefully this Stack Overflow question will help create that and help others.Flashing
@PauloMattos Using my answer above any ideas on this one?Flashing
D
0

This is similar to @Vini App's answer above but moves the struct Age inside Person so "hides" it, somewhat per OP's request not to add more data types. The answer here also provides a simpler interface: .age. Only the age itself needs to be optional.

// include the jsonData from the question here

struct Person: Decodable {
    let firstName, lastName: String
    private var _age: Age
    var age: String? {
        return _age.realage
    }

    struct Age: Decodable {
        let realage: String?
    }
    
    enum CodingKeys: String, CodingKey {
        case firstName = "firstname"
        case lastName = "lastname"
        case _age = "age"
    }
}


let people = try! JSONDecoder().decode([Person].self, from: jsonData)
// people.first._age // '_age' is inaccessible due to 'private' protection level
people.map { person in
    "\(person.firstName) \(person.lastName) is \(person.age ?? "??")"
}

(The map just makes seeing all the data easier in Playground.)

Denticle answered 23/5, 2024 at 19:49 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.