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

6

75

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"
}
Mckale answered 22/12, 2017 at 4:4 Comment(8)
I would suggest you use the SwiftyJSON library instead.Recent
Why do you say that? I really don't think it is good practice to uses SwiftyJSON now that codable is so much better. If there is no solution then obviously I will use SwiftyJSON but I would be surprised if there was no way for this to be done.Mckale
Can you give examples of which JSON works and which JSON doesn't?Roxannroxanna
yes, I have added it to the questionMckale
@NevinJethmalani You can provide your own decoder initializer and store the zero integer as String "0"Yeomanry
How would I do that for this scenario?Mckale
I think the best would be having this: var id:Any?, unfortunately is not possible. Anyway is possible to define a wrapper for the ambiguity, I implemented below a solution using enum MetadataType, which can map both String and Int.Repp
you can use this pod github.com/muhammadali2012/ModelGlimmer
Y
90
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)
}
Yeomanry answered 22/12, 2017 at 4:56 Comment(12)
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.Mckale
I don't think you can solve it without a custom decoderYeomanry
ok sounds good. completely understand. what are you thoughts on the other answer that was posted?Mckale
note that you should not assign nil to a implicitly unwrapped optionalYeomanry
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?Mckale
You can use regular optional as I did aboveYeomanry
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 cleanerMckale
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"Yeomanry
your ID property should be a String. You don't need to do any math with it.Yeomanry
Another option is to assign nil instead of "0" to your IDYeomanry
Unfortunately I am using a third party API so I don’t have control over thatMckale
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 stringYeomanry
R
38

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
}
Repp answered 22/12, 2017 at 4:59 Comment(7)
Great answer! If you want to access the associated value directly, here's some help.Piscatorial
Elegant Solution! This easily fixed my issue. Thanks!Rigi
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 thisBrassy
can someone explain this a bit more? more specifically the line enum MetadataType: Codable {Thigmotropism
How can i handle if value can be null or String or Int for the same key?Larina
Thanks for the clean, crisp example of type overriding for JSON!Hob
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 MetadataTypeBellda
I
31

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
Idonna answered 16/2, 2020 at 9:8 Comment(2)
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 ?Evelinaeveline
@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.Idonna
H
7

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

Hardandfast answered 15/6, 2020 at 17:3 Comment(2)
Awesome solution! Thanks!Venture
@Venture you are most welcome, Glad that I helpsHardandfast
I
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?
}
Illative answered 18/9, 2021 at 10:18 Comment(0)
G
0

you can use this pod https://github.com/muhammadali2012/Model

Simply add these property wrappers on your codable properties which type is not sure. ie

@AnyValueWrapper @DefaultStringEmpty var id: String

you will get id as String even if you get int from JSON or even nill or even if key doesn't exist.

Glimmer answered 13/10, 2023 at 13:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.