Search code examples
jsonswift4decodableswift4.1

Decodable and JSON, 2 datatypes for same variable


I'm using the Decodable protocol to decode some json, but I've run into a problem:

I'm getting an answer back, where a longitude and a latitide can be either an interger (latitude = 0) if there's no geo location data added to the element, and a String (fx. latitude = "25.047880") if there's geodata available. Now when I decode the json, I don't know how to build my Struct, as the long and lat can't both be String and Int.. So I'm getting a decode error when fetching elements where both cases are represented.

Any suggestions about how to solve this? I've tried with "Any" as datatype, but this doesn't conform to the Decodable protocol

struct JPhoto: Decodable {
  let id: String
  let farm: Int
  let secret: String
  let server: String
  let owner: String
  let title: String
  let latitude: String //Can both be Int and String
  let longitude: String //Can both be Int and String
}

Solution

  • You need to write your own encoder/decoder. You can use an associated value enum to do this, using a switch statement to encode and the throwing/catching behaviour to decode:

    enum AngularDistance:Codable {
        case string(String), integer(Int)
    
        func encode(to encoder: Encoder) throws {
            switch self {
            case .string(let str):
                var container = encoder.singleValueContainer()
                try container.encode(str)
            case .integer(let int):
                var container = encoder.singleValueContainer()
                try container.encode(int)
            }
        }
    
        init(from decoder: Decoder) throws {
            do {
                let container = try decoder.singleValueContainer()
                let str = try container.decode(String.self)
                self = AngularDistance.string(str)
            }
            catch {
                  do { let container = try decoder.singleValueContainer()
                       let int = try container.decode(Int.self)
                       self = AngularDistance.integer(int) 
                  }
                  catch {
                       throw DecodingError.typeMismatch(AngularDistance.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Expected to decode an Int or a String"))
                  }
            }
        }
    }
    

    Here's an example of encoding and decoding this AngularDistance type:

    let lat = [AngularDistance.string("String"), AngularDistance.integer(10)]
    let encoder = JSONEncoder()
    var decoder = JSONDecoder()
    
    do {
        let encoded = try encoder.encode(lat)
        try decoder.decode(Array<AngularDistance>.self, from: encoded)
    }
    catch DecodingError.typeMismatch(let t, let e)  {
        t
        e.codingPath
        e.debugDescription
    }
    catch {
        print(error.localizedDescription)
        }
    

    And here's your struct rewritten:

    struct JPhoto: Decodable {
      let id: String
      let farm: Int
      let secret: String
      let server: String
      let owner: String
      let title: String
      let latitude: AngularDistance //Can both be Int and String
      let longitude: AngularDistance //Can both be Int and String
    }