Swift structures: handling multiple types for a si

2019-02-07 09:18发布

问题:

I am using Swift 4 and trying to parse some JSON data which apparently in some cases can have different type values for the same key, e.g.:

{
    "type": 0.0
}

and

{
    "type": "12.44591406"
}

I am actually stuck with defining my struct because I cannot figure out how to handle this case because

struct ItemRaw: Codable {
    let parentType: String

    enum CodingKeys: String, CodingKey {
        case parentType = "type"
    }
}

throws "Expected to decode String but found a number instead.", and naturally,

struct ItemRaw: Codable {
    let parentType: Float

    enum CodingKeys: String, CodingKey {
        case parentType = "type"
    }
}

throws "Expected to decode Float but found a string/data instead." accordingly.

How can I handle this (and similar) cases when defining my struct?

回答1:

I ran into the same issue when trying to decode/encode the "edited" field on a Reddit Listing JSON response. I created a struct that represents the dynamic type that could exist for the given key. The key can have either a boolean or an integer.

{ "edited": false }
{ "edited": 123456 }

If you only need to be able to decode, just implement init(from:). If you need to go both ways, you will need to implement encode(to:) function.

struct Edited: Codable {
    let isEdited: Bool
    let editedTime: Int

    // Where we determine what type the value is
    init(from decoder: Decoder) throws {
        let container =  try decoder.singleValueContainer()

        // Check for a boolean
        do {
            isEdited = try container.decode(Bool.self)
            editedTime = 0
        } catch {
            // Check for an integer
            editedTime = try container.decode(Int.self)
            isEdited = true
        }
    }

    // We need to go back to a dynamic type, so based on the data we have stored, encode to the proper type
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try isEdited ? container.encode(editedTime) : container.encode(false)
    }
}

Inside my Codable class, I then use my struct.

struct Listing: Codable {
    let edited: Edited
}

Edit: A more specific solution for your scenario

I recommend using the CodingKey protocol and an enum to store all the properties when decoding. When you create something that conforms to Codable the compiler will create a private enum CodingKeys for you. This lets you decide on what to do based on the JSON Object property key.

Just for example, this is the JSON I am decoding:

{"type": "1.234"}
{"type": 1.234}

If you want to cast from a String to a Double because you only want the double value, just decode the string and then create a double from it. (This is what Itai Ferber is doing, you would then have to decode all properties as well using try decoder.decode(type:forKey:))

struct JSONObjectCasted: Codable {
    let type: Double?

    init(from decoder: Decoder) throws {
        // Decode all fields and store them
        let container = try decoder.container(keyedBy: CodingKeys.self) // The compiler creates coding keys for each property, so as long as the keys are the same as the property names, we don't need to define our own enum.

        // First check for a Double
        do {
            type = try container.decode(Double.self, forKey: .type)

        } catch {
            // The check for a String and then cast it, this will throw if decoding fails
            if let typeValue = Double(try container.decode(String.self, forKey: .type)) {
                type = typeValue
            } else {
                // You may want to throw here if you don't want to default the value(in the case that it you can't have an optional).
                type = nil
            }
        }

        // Perform other decoding for other properties.
    }
}

If you need to store the type along with the value, you can use an enum that conforms to Codable instead of the struct. You could then just use a switch statement with the "type" property of JSONObjectCustomEnum and perform actions based upon the case.

struct JSONObjectCustomEnum: Codable {
    let type: DynamicJSONProperty
}

// Where I can represent all the types that the JSON property can be. 
enum DynamicJSONProperty: Codable {
    case double(Double)
    case string(String)

    init(from decoder: Decoder) throws {
        let container =  try decoder.singleValueContainer()

        // Decode the double
        do {
            let doubleVal = try container.decode(Double.self)
            self = .double(doubleVal)
        } catch DecodingError.typeMismatch {
            // Decode the string
            let stringVal = try container.decode(String.self)
            self = .string(stringVal)
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .double(let value):
            try container.encode(value)
        case .string(let value):
            try container.encode(value)
        }
    }
}


回答2:

One simple solution is to provide an implementation of init(from:) which attempts to decode the value as a String, and if that fails because the type is wrong, attempt to decode as a Double:

public init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    do {
        self.parentType = try container.decode(String.self, forKey: .parentType)
    } catch DecodingError.typeMismatch {
        let value = try container.decode(Double.self, forKey: .parentType)
        self.parentType = "\(value)"
    }
}


回答3:

I had to decode PHP/MySQL/PDO double value that is given as an String, for this use-case I had to extend the KeyedDecodingContainer, like so:

extension KeyedDecodingContainer {
    func decode(forKey key: KeyedDecodingContainer.Key) throws -> Double {
        do {
            let str = try self.decode(String.self, forKey: key)
            if let dbl = Double(str) {
                return dbl
            }
        } catch DecodingError.typeMismatch {
            return try self.decode(Double.self, forKey: key)
        }
        let context = DecodingError.Context(codingPath: self.codingPath,
                                            debugDescription: "Wrong Money Value")
        throw DecodingError.typeMismatch(Double.self, context)
    }
}

Usage:

let data = """
{"value":"1.2"}
""".data(using: .utf8)!

struct Test: Decodable {
    let value: Double
    enum CodingKeys: String, CodingKey {
        case value
    }
    init(from decoder: Decoder) throws {
        self.value = try decoder.container(keyedBy: CodingKeys.self)
                                .decode(forKey: CodingKeys.value)
    }
}
try JSONDecoder().decode(Test.self, from: data).value