Decodable and JSON, 2 datatypes for same variable

2019-04-02 10:23发布

问题:

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
}

回答1:

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
}