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 (postco
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))"
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.
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)
}
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 Int
s 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)"
}
}
}