How to Parse Json With Decodable Protocol When Property Types Might Change from Int to String

How to decode a property with type of JSON dictionary in Swift [45] decodable protocol

With some inspiration from this gist I found, I wrote some extensions for UnkeyedDecodingContainer and KeyedDecodingContainer. You can find a link to my gist here. By using this code you can now decode any Array<Any> or Dictionary<String, Any> with the familiar syntax:

let dictionary: [String: Any] = try container.decode([String: Any].self, forKey: key)

or

let array: [Any] = try container.decode([Any].self, forKey: key)

Edit: there is one caveat I have found which is decoding an array of dictionaries [[String: Any]] The required syntax is as follows. You'll likely want to throw an error instead of force casting:

let items: [[String: Any]] = try container.decode(Array<Any>.self, forKey: .items) as! [[String: Any]]

EDIT 2: If you simply want to convert an entire file to a dictionary, you are better off sticking with api from JSONSerialization as I have not figured out a way to extend JSONDecoder itself to directly decode a dictionary.

guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
// appropriate error handling
return
}

The extensions

// Inspired by https://gist.github.com/mbuchetics/c9bc6c22033014aa0c550d3b4324411a

struct JSONCodingKeys: CodingKey {
var stringValue: String

init?(stringValue: String) {
self.stringValue = stringValue
}

var intValue: Int?

init?(intValue: Int) {
self.init(stringValue: "\(intValue)")
self.intValue = intValue
}
}


extension KeyedDecodingContainer {

func decode(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any> {
let container = try self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key)
return try container.decode(type)
}

func decodeIfPresent(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any>? {
guard contains(key) else {
return nil
}
guard try decodeNil(forKey: key) == false else {
return nil
}
return try decode(type, forKey: key)
}

func decode(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any> {
var container = try self.nestedUnkeyedContainer(forKey: key)
return try container.decode(type)
}

func decodeIfPresent(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any>? {
guard contains(key) else {
return nil
}
guard try decodeNil(forKey: key) == false else {
return nil
}
return try decode(type, forKey: key)
}

func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
var dictionary = Dictionary<String, Any>()

for key in allKeys {
if let boolValue = try? decode(Bool.self, forKey: key) {
dictionary[key.stringValue] = boolValue
} else if let stringValue = try? decode(String.self, forKey: key) {
dictionary[key.stringValue] = stringValue
} else if let intValue = try? decode(Int.self, forKey: key) {
dictionary[key.stringValue] = intValue
} else if let doubleValue = try? decode(Double.self, forKey: key) {
dictionary[key.stringValue] = doubleValue
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedDictionary
} else if let nestedArray = try? decode(Array<Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedArray
}
}
return dictionary
}
}

extension UnkeyedDecodingContainer {

mutating func decode(_ type: Array<Any>.Type) throws -> Array<Any> {
var array: [Any] = []
while isAtEnd == false {
// See if the current value in the JSON array is `null` first and prevent infite recursion with nested arrays.
if try decodeNil() {
continue
} else if let value = try? decode(Bool.self) {
array.append(value)
} else if let value = try? decode(Double.self) {
array.append(value)
} else if let value = try? decode(String.self) {
array.append(value)
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self) {
array.append(nestedDictionary)
} else if let nestedArray = try? decode(Array<Any>.self) {
array.append(nestedArray)
}
}
return array
}

mutating func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {

let nestedContainer = try self.nestedContainer(keyedBy: JSONCodingKeys.self)
return try nestedContainer.decode(type)
}
}

How to decode a JSON property with different types?

You will have to implement your own func encode(to encoder: Encoder) throws and init(from decoder: Decoder) throws which are both properties of the Codable protocol. Then change your rating variable into an enum

Which would look like this:

enum Rating: Codable {
case int(Int)
case string(String)

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .int(let v): try container.encode(v)
case .string(let v): try container.encode(v)
}
}

init(from decoder: Decoder) throws {
let value = try decoder.singleValueContainer()

if let v = try? value.decode(Int.self) {
self = .int(v)
return
} else if let v = try? value.decode(String.self) {
self = .string(v)
return
}

throw Rating.ParseError.notRecognizedType(value)
}

enum ParseError: Error {
case notRecognizedType(Any)
}
}

Then on your DetailModel just change rating: String to rating: Rating

This works, I have tested with these JSON strings.

// int rating
{
"rating": 200,
"bro": "Success"
}

// string rating
{
"rating": "200",
"bro": "Success"
}

Edit: I've found a better swiftier way of implementing init(from decoder: Decoder) throws, which produces a better error message, by using this you can now omit the ParseError enum.

init(from decoder: Decoder) throws {
let value = try decoder.singleValueContainer()
do {
self = .int(try value.decode(Int.self))
} catch DecodingError.typeMismatch {
self = .string(try value.decode(String.self))
}
}

Make Swift JSONDecoder not fail when type on key not match

Implement init(from:) in struct Object. Create enum CodingKeys and add the cases for all the properties you want to parse.

In init(from:) parse the keys manually and check if year from JSON can be decoded as an Int. If yes, assign it to the Object's year property otherwise don't.

struct Object: Codable {
var year: Int?

enum CodingKeys: String,CodingKey {
case year
}

init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
if let yr = try? values.decodeIfPresent(Int.self, forKey: .year) {
year = yr
}
}
}

Parse the JSON response like,

do {
let object = try JSONDecoder().decode(Object.self, from: data)
print(object)
} catch {
print(error)
}

Example:

  1. If JSON is { "year": "10"}, object is: Object(year: nil)
  2. If JSON is { "year": 10}, object is: Object(year: Optional(10))

Using codable with value that is sometimes an Int and other times a String

struct GeneralProduct: Codable {
var price: Double?
var id: String?
var name: String?
private enum CodingKeys: String, CodingKey {
case price = "p", id = "i", name = "n"
}
init(price: Double? = nil, id: String? = nil, name: String? = nil) {
self.price = price
self.id = id
self.name = name
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
price = try container.decode(Double.self, forKey: .price)
name = try container.decode(String.self, forKey: .name)
do {
id = try String(container.decode(Int.self, forKey: .id))
} catch DecodingError.typeMismatch {
id = try container.decode(String.self, forKey: .id)
}
}
}


let json1 = """
{
"p":2.12,
"i":"3k3mkfnk3",
"n":"Blue Shirt"
}
"""

let json2 = """
{
"p":2.12,
"i":0,
"n":"Blue Shirt"
}
"""


do {
let product = try JSONDecoder().decode(GeneralProduct.self, from: Data(json2.utf8))
print(product.price ?? "nil")
print(product.id ?? "nil")
print(product.name ?? "nil")
} catch {
print(error)
}

edit/update:

You can also simply assign nil to your id when your api returns 0:

do {
let value = try container.decode(Int.self, forKey: .id)
id = value == 0 ? nil : String(value)
} catch DecodingError.typeMismatch {
id = try container.decode(String.self, forKey: .id)
}

parse JSON with swift via a model to object - decode int/string trouble

This wasn't the problem that the error message gave!

All I needed to do to fix the problem was to employ CodingKeys.

I was hoping to avoid this since the data structure (JSON) had lots of members. But this fixed the problem.

Now an example of my model:

struct Product
{
let name: String
let long_name_value: String

...

    enum MemberKeys: String, CodingKey
{
case name
case longNameValue = "long_name_value"

...

    }
}

I guess the reason is swift doesn't like snake case (eg. "long_name_value"), so I needed to convert it to camel case (eg."longNameValue"). Then the errors disappeared.

How to decode a nested JSON struct with Swift Decodable protocol?

Another approach is to create an intermediate model that closely matches the JSON (with the help of a tool like quicktype.io), let Swift generate the methods to decode it, and then pick off the pieces that you want in your final data model:

// snake_case to match the JSON and hence no need to write CodingKey enums
fileprivate struct RawServerResponse: Decodable {
struct User: Decodable {
var user_name: String
var real_info: UserRealInfo
}

struct UserRealInfo: Decodable {
var full_name: String
}

struct Review: Decodable {
var count: Int
}

var id: Int
var user: User
var reviews_count: [Review]
}

struct ServerResponse: Decodable {
var id: String
var username: String
var fullName: String
var reviewCount: Int

init(from decoder: Decoder) throws {
let rawResponse = try RawServerResponse(from: decoder)

// Now you can pick items that are important to your data model,
// conveniently decoded into a Swift structure
id = String(rawResponse.id)
username = rawResponse.user.user_name
fullName = rawResponse.user.real_info.full_name
reviewCount = rawResponse.reviews_count.first!.count
}
}

This also allows you to easily iterate through reviews_count, should it contain more than 1 value in the future.

Swift 4 JSON Decodable simplest way to decode type change

Unfortunately, I don't believe such an option exists in the current JSONDecoder API. There only exists an option in order to convert exceptional floating-point values to and from a string representation.

Another possible solution to decoding manually is to define a Codable wrapper type for any LosslessStringConvertible that can encode to and decode from its String representation:

struct StringCodableMap<Decoded : LosslessStringConvertible> : Codable {

var decoded: Decoded

init(_ decoded: Decoded) {
self.decoded = decoded
}

init(from decoder: Decoder) throws {

let container = try decoder.singleValueContainer()
let decodedString = try container.decode(String.self)

guard let decoded = Decoded(decodedString) else {
throw DecodingError.dataCorruptedError(
in: container, debugDescription: """
The string \(decodedString) is not representable as a \(Decoded.self)
"""
)
}

self.decoded = decoded
}

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(decoded.description)
}
}

Then you can just have a property of this type and use the auto-generated Codable conformance:

struct Example : Codable {

var name: String
var age: Int
var taxRate: StringCodableMap<Float>

private enum CodingKeys: String, CodingKey {
case name, age
case taxRate = "tax_rate"
}
}

Although unfortunately, now you have to talk in terms of taxRate.decoded in order to interact with the Float value.

However you could always define a simple forwarding computed property in order to alleviate this:

struct Example : Codable {

var name: String
var age: Int

private var _taxRate: StringCodableMap<Float>

var taxRate: Float {
get { return _taxRate.decoded }
set { _taxRate.decoded = newValue }
}

private enum CodingKeys: String, CodingKey {
case name, age
case _taxRate = "tax_rate"
}
}

Although this still isn't as a slick as it really should be – hopefully a later version of the JSONDecoder API will include more custom decoding options, or else have the ability to express type conversions within the Codable API itself.

However one advantage of creating the wrapper type is that it can also be used in order to make manual decoding and encoding simpler. For example, with manual decoding:

struct Example : Decodable {

var name: String
var age: Int
var taxRate: Float

private enum CodingKeys: String, CodingKey {
case name, age
case taxRate = "tax_rate"
}

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

self.name = try container.decode(String.self, forKey: .name)
self.age = try container.decode(Int.self, forKey: .age)
self.taxRate = try container.decode(StringCodableMap<Float>.self,
forKey: .taxRate).decoded
}
}


Related Topics



Leave a reply



Submit