4

I have to decode a JSON with a big structure and a lot of nested arrays. I have reproduced the structure in my UserModel file, and it works, except with one property (postcode) that is in a nested array (Location) that sometimes is an Int and some other is a String. I don't know how to handle this situation and tried a lot of different solutions. The last one I've tried is from this blog https://agostini.tech/2017/11/12/swift-4-codable-in-real-life-part-2/ And it suggests using generics. But now I can't initialize the Location object without providing a Decoder():

enter image description here

Any help or any different approach would be appreciated. The API call is this one: https://api.randomuser.me/?results=100&seed=xmoba This is my UserModel File:

import Foundation
import UIKit
import ObjectMapper

struct PostModel: Equatable, Decodable{

    static func ==(lhs: PostModel, rhs: PostModel) -> Bool {
        if lhs.userId != rhs.userId {
            return false
        }
        if lhs.id != rhs.id {
            return false
        }
        if lhs.title != rhs.title {
            return false
        }
        if lhs.body != rhs.body {
            return false
        }
        return true
    }


    var userId : Int
    var id : Int
    var title : String
    var body : String

    enum key : CodingKey {
        case userId
        case id
        case title
        case body
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: key.self)
        let userId = try container.decode(Int.self, forKey: .userId)
        let id = try container.decode(Int.self, forKey: .id)
        let title = try container.decode(String.self, forKey: .title)
        let body = try container.decode(String.self, forKey: .body)

        self.init(userId: userId, id: id, title: title, body: body)
    }

    init(userId : Int, id : Int, title : String, body : String) {
        self.userId = userId
        self.id = id
        self.title = title
        self.body = body
    }
    init?(map: Map){
        self.id = 0
        self.title = ""
        self.body = ""
        self.userId = 0
    }
}

extension PostModel: Mappable {



    mutating func mapping(map: Map) {
        id       <- map["id"]
        title     <- map["title"]
        body     <- map["body"]
        userId     <- map["userId"]
    }

}
Reza Mousavi
  • 4,420
  • 5
  • 31
  • 48
Alfro
  • 1,524
  • 2
  • 21
  • 37
  • Unrelated to your issue, but the `==` function can be simplified to `static func ==(lhs: PostModel, rhs: PostModel) -> Bool { return lhs.userId == rhs.userId && lhs.id == rhs.id && lhs.title == rhs.title && lhs.body == rhs.body }`. Your current `init(from:)` method is also unnecessary, the compiler can synthetise it automatically, the same is true for your `init(userId:, id:, title:, body:)` method. – Dávid Pásztor Oct 03 '18 at 16:05
  • Better than nothing indeed, thanks – Alfro Oct 03 '18 at 16:07
  • 1
    In Swift 4.1+ even the explicit `static ==` function is synthesized if all properties are going to be compared. – vadian Oct 03 '18 at 16:24
  • @Larme it is not the same, this Json has nested arrays that and the way you get access to the properties is different than in the duplicate question you provide. – Alfro Oct 03 '18 at 16:31
  • @user3033437 It's the same one. At some point you need a Struct named `Location` which is decodable which will has a `postcode` property set either to String or Int, and test what's done in the related question. – Larme Oct 03 '18 at 16:42

4 Answers4

4

Well it's a common IntOrString problem. You could just make your property type an enum that can handle either String or Int.

enum IntOrString: Codable {
    case int(Int)
    case string(String)
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        do {
            self = try .int(container.decode(Int.self))
        } catch DecodingError.typeMismatch {
            do {
                self = try .string(container.decode(String.self))
            } catch DecodingError.typeMismatch {
                throw DecodingError.typeMismatch(IntOrString.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Encoded payload conflicts with expected type, (Int or String)"))
            }
        }
    }
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .int(let int):
            try container.encode(int)
        case .string(let string):
            try container.encode(string)
        }
    }
}

As I have found mismatch of your model that you posted in your question and the one in the API endpoint you pointed to, I've created my own model and own JSON that needs to be decoded.

struct PostModel: Decodable {
    let userId: Int
    let id: Int
    let title: String
    let body: String
    let postCode: IntOrString
    // you don't need to implement init(from decoder: Decoder) throws
    // because all the properties are already Decodable
}

Decoding when postCode is Int:

let jsonData = """
{
"userId": 123,
"id": 1,
"title": "Title",
"body": "Body",
"postCode": 9999
}
""".data(using: .utf8)!
do {
    let postModel = try JSONDecoder().decode(PostModel.self, from: jsonData)
    if case .int(let int) = postModel.postCode {
        print(int) // prints 9999
    } else if case .string(let string) = postModel.postCode {
        print(string)
    }
} catch {
    print(error)
}

Decoding when postCode is String:

let jsonData = """
{
"userId": 123,
"id": 1,
"title": "Title",
"body": "Body",
"postCode": "9999"
}
""".data(using: .utf8)!
do {
    let postModel = try JSONDecoder().decode(PostModel.self, from: jsonData)
    if case .int(let int) = postModel.postCode {
        print(int)
    } else if case .string(let string) = postModel.postCode {
        print(string) // prints "9999"
    }
} catch {
    print(error)
}
nayem
  • 7,285
  • 1
  • 33
  • 51
3

You can use generic like this:

enum Either<L, R> {
    case left(L)
    case right(R)
}

extension Either: Decodable where L: Decodable, R: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let left = try? container.decode(L.self) {
            self = .left(left)
        } else if let right = try? container.decode(R.self) {
            self = .right(right)
        } else {
            throw DecodingError.typeMismatch(Either<L, R>.self, .init(codingPath: decoder.codingPath, debugDescription: "Expected either `\(L.self)` or `\(R.self)`"))
        }
    }
}

extension Either: Encodable where L: Encodable, R: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case let .left(left):
            try container.encode(left)
        case let .right(right):
            try container.encode(right)
        }
    }
}

And then declare postcode: Either<Int, String> and if your model is Decodable and all other fields are Decodable too no extra code would be needed.

user28434'mstep
  • 6,290
  • 2
  • 20
  • 35
0

If postcode can be both String and Int, you have (at least) two possible solutions for this issue. Firstly, you can simply store all postcodes as String, since all Ints can be converted to String. This seems like the best solution, since it seems highly unlikely that you'd need to perform any numeric operations on a postcode, especially if some postcodes can be String. The other solution would be creating two properties for postcode, one of type String? and one of type Int? and always only populating one of the two depending on the input data, as explained in Using codable with key that is sometimes an Int and other times a String.

The solution storing all postcodes as String:

struct PostModel: Equatable, Decodable {
    static func ==(lhs: PostModel, rhs: PostModel) -> Bool {
        return lhs.userId == rhs.userId && lhs.id == rhs.id && lhs.title == rhs.title && lhs.body == rhs.body
    }

    var userId: Int
    var id: Int
    var title: String
    var body: String
    var postcode: String

    enum CodingKeys: String, CodingKey {
        case userId, id, title, body, postcode
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.userId = try container.decode(Int.self, forKey: .userId)
        self.id = try container.decode(Int.self, forKey: .id)
        self.title = try container.decode(String.self, forKey: .title)
        self.body = try container.decode(String.self, forKey: .body)
        if let postcode = try? container.decode(String.self, forKey: .postcode) {
            self.postcode = postcode
        } else {
            let numericPostcode = try container.decode(Int.self, forKey: .postcode)
            self.postcode = "\(numericPostcode)"
        }
    }
}
Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
  • doesn't work like that, this is not the kind of structure I have, I instantiate [User].self and inside user there is a nested Location() object wich have the property postcode. So when init(from decoder:throws), everything happens in this line: let users = try container.decode([User].self, forKey: .results) – Alfro Oct 03 '18 at 16:24
  • @user3033437 then why did you include irrelevant code in your question? You should edit your question with the actual code causing the issue and some sample JSON responses – Dávid Pásztor Oct 03 '18 at 16:26
  • There is no way I can provide Json sample response from the app, except the error, which is already posted. The json is never formed because the app crash when postcode is String instead of Int or viceversa! – Alfro Oct 03 '18 at 16:27
  • @user3033437 you can easily print the JSON response before parsing it, set up exception breakpoints or simply use `try?` to avoid a crash for the sake of retrieving the JSON response – Dávid Pásztor Oct 03 '18 at 16:32
0

try this extension

extension KeyedDecodingContainer{
    public func decodeIfPresent(_ type: String.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> String?{
        if let resStr = try? decode(type, forKey: key){
            return resStr
        }else{
            if let resInt = try? decode(Int.self, forKey: key){
                return String(resInt)
            }
            return nil
        }
    }

    public func decodeIfPresent(_ type: Int.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Int?{
        if let resInt = try? decode(type, forKey: key){
            return resInt
        }else{
            if let resStr = try? decode(String.self, forKey: key){
                return Int(resStr)
            }
            return nil
        }
    }
}

example

struct Foo:Codable{
    let strValue:String?
    let intValue:Int?
}

let data = """
{
"strValue": 1,
"intValue": "1"
}
""".data(using: .utf8)
print(try? JSONDecoder().decode(Foo.self, from: data!))

it will print "Foo(strValue: Optional("1"), intValue: Optional(1))"

Elijah
  • 1
  • 1