3

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
}
Nicolai Harbo
  • 1,064
  • 12
  • 25
  • You have to write a custom initializer to handle the cases. Please read [Encoding and Decoding Custom Types](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types) – vadian Apr 08 '18 at 12:42

2 Answers2

6

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
}
sketchyTech
  • 5,746
  • 1
  • 33
  • 56
  • Thanks for your comment! It makes sense, most of it! What i'm a little confused about is, if I need the middle codesnippet? Like, I just need to add the enum, and then rewrite my struct, right ? :-) – Nicolai Harbo Apr 08 '18 at 13:17
  • 1
    On a side note, you didn't handle the error cases. What if the value returned isn't a `String` or `Int`? You should handle those scenarios by throwing `DecodingError` explicitly. – nayem Apr 08 '18 at 13:58
  • @nayem As with all the other properties in the `Codable` struct, if the value doesn't exist or if it is a different type then an error will be thrown when the JSONDecoder instance attempts to decode. Codable simply won't parse an incorrectly formatted string and throws the error "The data couldn’t be read because it isn’t in the correct format." – sketchyTech Apr 08 '18 at 16:39
  • @NicolaiHarbo yes, you just rewrite the struct but need to be aware when handling the received values that you need to extract the [associated values](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Enumerations.html). – sketchyTech Apr 08 '18 at 16:45
  • While this is true what you said. But it isn't explanatory description, is it? It's pretty vague. – nayem Apr 08 '18 at 17:27
  • You can add error handling if you wish but I wouldn't view it as essential unless you are going to test every type in the same way. So yes if you are adding error handling to String, Int, Bool, Dictionary and Array then this could be useful for debugging. But to just isolate one type and say we must add error handling isn't going to be particularly useful on its own. – sketchyTech Apr 08 '18 at 18:33
  • Why not? Okay let's try to decode a `Bool` and tell me what error are you getting? Then I will show you how to improve that. – nayem Apr 08 '18 at 18:42
  • 1
    @nayem OK, so there's a DecodingError enum that I wasn't aware of. Thanks for prompting me to research this. I've added rudimentary error support to my example based on this. – sketchyTech Apr 08 '18 at 19:22
  • 1
    Yes! That's it. I was pointing you towards that. Glad that you figured it out. – nayem Apr 09 '18 at 02:46
-1

There are couple of approaches to mention in addition to enums with associated value. You can use proposed Either<Int, String> or IntOrString struct for your latitude and longitude.

struct Either<F: Codable, S: Codable>: Codable {
    var firstValue: F?
    var secondValue: S?

    var value: Any? {
        return firstValue ?? secondValue
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        if firstValue != nil {
            try? container.encode(firstValue)
        } else {
            try? container.encode(secondValue)
        }
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        firstValue = try? container.decode(F.self)
        secondValue = try? container.decode(S.self)
        if firstValue == nil && secondValue == nil {
            //Type mismatch
            throw DecodingError.typeMismatch(type(of: self), DecodingError.Context(codingPath: [], debugDescription: "The value is not of type \(F.self) and also not \(S.self)"))
        }
    }
}

Another way to do the same:

struct IntOrString: Codable {
    
    var value: Any
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        if  let intValue = value as? Int {
            try? container.encode(intValue)
        } else if let strValue = value as? String {
            try? container.encode(strValue)
        }
    }
    
    init(from decoder: Decoder) throws {
        if let int = try? Int(from: decoder) {
            value = int
            return
        }
        value = try String(from: decoder)
    }
}
Paul B
  • 3,989
  • 33
  • 46