jsonswiftdata-structuresswift4swift-structs

Swift structures: handling multiple types for a single property


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?


Solution

  • 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)
            }
        }
    }