69

I have an API that will sometimes return a specific key value (in this case id) in the JSON as an Int and other times it will return that same key value as a String. How do I use codable to parse that JSON?

struct GeneralProduct: Codable {
    var price: Double!
    var id: String?
    var name: String!

    private enum CodingKeys: String, CodingKey {
        case price = "p"
        case id = "i"
        case name = "n"
    }

    init(price: Double? = nil, id: String? = nil, name: String? = nil) {
        self.price = price
        self.id = id
        self.name = name
    }
}

I keep getting this error message: Expected to decode String but found a number instead. The reason that it returns a number is because the id field is empty and when the id field is empty it defaults to returning 0 as an ID which codable identifies as a number. I can basically ignore the ID key but codable does not give me the option to ignore it to my knowledge. What would be the best way to handle this?

Here is the JSON. It is super simple

Working

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

Error - because there is no id in the system, it returns 0 as a default which codable obviously sees as a number opposed to string.

{
  "p":2.19,
  "i":0,
  "n":"Black Shirt"
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
Nevin Jethmalani
  • 2,726
  • 4
  • 23
  • 58

5 Answers5

85
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)
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • I believe this is what I am looking for but the real scenario is much more complicated with a lot more variable. there is no way to do this without writing a custom initializer for each variable? It is just a lot of extra code for something so small. – Nevin Jethmalani Dec 22 '17 at 04:59
  • I don't think you can solve it without a custom decoder – Leo Dabus Dec 22 '17 at 05:00
  • ok sounds good. completely understand. what are you thoughts on the other answer that was posted? – Nevin Jethmalani Dec 22 '17 at 05:00
  • note that you should not assign nil to a implicitly unwrapped optional – Leo Dabus Dec 22 '17 at 05:00
  • this is definitely something that has confused me. what are the best practices for initializing something like this. I know that this is dependent on the situation but at a high level what I am trying to do is in some scenarios, I want an empty instance of the struct so I use `GeneralProduct ()` the only way for that to work with codable is to use that initializer. What is a better way to do it? – Nevin Jethmalani Dec 22 '17 at 05:02
  • You can use regular optional as I did above – Leo Dabus Dec 22 '17 at 05:03
  • awesome thanks a lot! what are you thoughts on the solution using an enum? Does that work as well? It looks like it might be a little cleaner – Nevin Jethmalani Dec 22 '17 at 05:05
  • I think that you should use what you think that works better in your scenario. I don't think you need a custom encoder just for the "0" – Leo Dabus Dec 22 '17 at 05:07
  • your ID property should be a String. You don't need to do any math with it. – Leo Dabus Dec 22 '17 at 05:08
  • Another option is to assign nil instead of "0" to your ID – Leo Dabus Dec 22 '17 at 05:26
  • Unfortunately I am using a third party API so I don’t have control over that – Nevin Jethmalani Dec 22 '17 at 05:45
  • 1
    You didn’t understand. I suggested adding nil to the ID instead of “0” which it is exactly what it is the lack of a ID string – Leo Dabus Dec 22 '17 at 05:46
34

This is a possible solution with MetadataType, the nice thing is that can be a general solution not for GeneralProduct only, but for all the struct having the same ambiguity:

struct GeneralProduct: Codable {
  var price:Double?
  var id:MetadataType?
  var name:String?

  private enum CodingKeys: String, CodingKey {
    case price = "p"
    case id = "i"
    case name = "n"
  }

  init(price:Double? = nil, id: MetadataType? = nil, name: String? = nil) {
    self.price = price
    self.id = id
    self.name = name
  }
}

enum MetadataType: 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(MetadataType.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Encoded payload not of an expected type"))
      }
    }
  }

  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)
    }
  }
}

this is the test:

let decoder = JSONDecoder()
var json =  "{\"p\":2.19,\"i\":0,\"n\":\"Black Shirt\"}"
var product = try! decoder.decode(GeneralProduct.self, from: json.data(using: .utf8)!)
if let id = product.id {
  print(id) // 0
}

json =  "{\"p\":2.19,\"i\":\"hello world\",\"n\":\"Black Shirt\"}"
product = try! decoder.decode(GeneralProduct.self, from: json.data(using: .utf8)!)
if let id = product.id {
  print(id) // hello world
}
mugx
  • 9,869
  • 3
  • 43
  • 55
  • 2
    Great answer! If you want to access the associated value directly, [here's some help](https://stackoverflow.com/q/24263539/3151675). – Tamás Sengel Mar 20 '18 at 10:16
  • 1
    Elegant Solution! This easily fixed my issue. Thanks! – Rudy B Jan 03 '19 at 18:32
  • if let id = product.id { print(id) // 0 } , I want to convert this id value to Float, but it is of Metadatatype , Can anyone give idea about this...or how should I use encoder function to get int value from this – sumit kapoor Feb 19 '19 at 12:39
  • can someone explain this a bit more? more specifically the line enum MetadataType: Codable { – MF DOOM May 01 '19 at 23:24
  • How can i handle if value can be null or String or Int for the same key? – Baibhav Singh May 27 '19 at 07:20
  • 1
    Thanks for the clean, crisp example of type overriding for JSON! – John Kaster Oct 30 '19 at 00:32
  • 7
    Does this not print out `int(0)` instead of `0`? As in the type assigned in the line `if let id = product.id` is still of type `MetadataType` – ErikAGriffin Nov 10 '19 at 21:54
30

Seamlessly decoding from either Int or String into the same property requires writing some code.

However, thanks to a (somewhat) new addition to the language,(property wrappers), you can make it quite easy to reuse this logic wherever you need it:

// note this is only `Decodable`
struct GeneralProduct: Decodable {
    var price: Double
    @Flexible var id: Int // note this is an Int
    var name: String
}

The property wrapper and its supporting code can be implemented like this:

@propertyWrapper struct Flexible<T: FlexibleDecodable>: Decodable {
    var wrappedValue: T
    
    init(from decoder: Decoder) throws {
        wrappedValue = try T(container: decoder.singleValueContainer())
    }
}

protocol FlexibleDecodable {
    init(container: SingleValueDecodingContainer) throws
}

extension Int: FlexibleDecodable {
    init(container: SingleValueDecodingContainer) throws {
        if let int = try? container.decode(Int.self) {
            self = int
        } else if let string = try? container.decode(String.self), let int = Int(string) {
            self = int
        } else {
            throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: "Invalid int value"))
        }
    }
}

Original answer

You can use a wrapper over a string that knows how to decode from any of the basic JSON data types: string, number, boolean:

struct RelaxedString: Codable {
    let value: String
    
    init(_ value: String) {
        self.value = value
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        // attempt to decode from all JSON primitives
        if let str = try? container.decode(String.self) {
            value = str
        } else if let int = try? container.decode(Int.self) {
            value = int.description
        } else if let double = try? container.decode(Double.self) {
            value = double.description
        } else if let bool = try? container.decode(Bool.self) {
            value = bool.description
        } else {
            throw DecodingError.typeMismatch(String.self, .init(codingPath: decoder.codingPath, debugDescription: ""))
        }
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(value)
    }
}

You can then use this new type in your struct. One minor disadvantage would be that consumer of the struct will need to make another indirection to access the wrapped string. However that can be avoided by declaring the decoded RelaxedString property as private, and use a computed one for the public interface:

struct GeneralProduct: Codable {
    var price: Double!
    var _id: RelaxedString?
    var name: String!
    
    var id: String? {
        get { _id?.value }
        set { _id = newValue.map(RelaxedString.init) }
    }

    private enum CodingKeys: String, CodingKey {
        case price = "p"
        case _id = "i"
        case name = "n"
    }

    init(price: Double? = nil, id: String? = nil, name: String? = nil) {
        self.price = price
        self._id = id.map(RelaxedString.init)
        self.name = name
    }
}

Advantages of the above approach:

  1. no need to write custom init(from decoder: Decoder) code, which can become tedious if the number of properties to be decoded increase
  2. reusability - RelaxedString can be seamlessly used in other structs
  3. the fact that the id can be decoded from a string or an int remains an implementation detail, consumers of GeneralProduct don't know/care that the id can come from a string or an int
  4. the public interface exposes string values, which keeps the consumer code simple as it will not have to deal with multiple types of data
Cristik
  • 30,989
  • 25
  • 91
  • 127
  • If key is missing from response in that case its failing for that i made wrapped value as optional but its still failing. Any other way to handle this ? – Surjeet Rajput Jun 26 '23 at 05:36
  • @SurjeetRajput not sure if there's a simple approach with property wrappers, in case you want to make the property optional. However, you can follow my first solution, the one with `RelaxedString` from the second half of the answer. – Cristik Jul 01 '23 at 08:01
6

I created this Gist which has a ValueWrapper struct that can handle the following types

case stringValue(String)
case intValue(Int)
case doubleValue(Double)
case boolValue(Bool)

https://gist.github.com/amrangry/89097b86514b3477cae79dd28bba3f23

koen
  • 5,383
  • 7
  • 50
  • 89
Amr Angry
  • 3,711
  • 1
  • 45
  • 37
4

Based on @Cristik 's answer, I come with another solution using @propertyWrapper.

@propertyWrapper
struct StringForcible: Codable {
    
    var wrappedValue: String?
    
    enum CodingKeys: CodingKey {}
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let string = try? container.decode(String.self) {
            wrappedValue = string
        } else if let integer = try? container.decode(Int.self) {
            wrappedValue = "\(integer)"
        } else if let double = try? container.decode(Double.self) {
            wrappedValue = "\(double)"
        } else if container.decodeNil() {
            wrappedValue = nil
        }
        else {
            throw DecodingError.typeMismatch(String.self, .init(codingPath: container.codingPath, debugDescription: "Could not decode incoming value to String. It is not a type of String, Int or Double."))
        }
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(wrappedValue)
    }
    
    init() {
        self.wrappedValue = nil
    }
    
}

And usage is

struct SomeDTO: Codable {
   @StringForcible var id: String? 
}

Also works like -I think-

struct AnotherDTO: Codable {
    var some: SomeDTO?
}
Faruk
  • 2,269
  • 31
  • 42