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?

errata
  • 5,695
  • 10
  • 54
  • 99

4 Answers4

23

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)
        }
    }
}
Latsu
  • 346
  • 1
  • 5
  • `try container.encode(isEdited ? editedTime : false)` – Leo Dabus Oct 15 '17 at 23:52
  • @LeoDabus, It would be nice if that expression was valid. The Swift 4 compiler doesn't allow mismatching types when using the ternary operator. – Latsu Oct 16 '17 at 03:09
  • 1
    this should work `try isEdited ? container.encode(editedTime) : container.encode(false)` – Leo Dabus Oct 16 '17 at 03:21
  • 1
    You are indeed correct, thanks for the heads up. I will update the snippet with your suggestion. – Latsu Oct 16 '17 at 03:36
  • I think you should catch any error also when decoding editedTime. It doesn't look good but I thin the correct syntax would be `do { isEdited = try container.decode(Bool.self) editedTime = 0 } catch { do { editedTime = try container.decode(Int.self) isEdited = true } catch { editedTime = 0 isEdited = false } }` – Leo Dabus Oct 16 '17 at 03:53
  • Great! This actually works and it is well explained. Just to ask something, following this procedure, if your `struct Edited` would have only 1 property, let's say `edited`, you would access your property like this: `MyObject.edited.edited`. Is there a way to define the `struct` so you can access the same property like: `MyObject.edited` only? – errata Oct 16 '17 at 10:24
  • @errata I initially misunderstood your comment. If you know and want to only store one type you can specifically look for the string and then cast it to the stored type using the decoder. Ill update my answer with a more clean solution for both scenarios. – Latsu Oct 16 '17 at 17:43
  • @errata, I've added another portion to my answer, does that help explain it? – Latsu Oct 16 '17 at 21:10
  • @Latsu how to access the parameter after this conversion. i am getting double and int, and i want to compare whether its value is zero or not? – Dhanunjay Kumar Jun 04 '19 at 09:09
  • @DhanunjayKumar Because of the enum mentioned fundamentally being a `union type` in the code I provided, you will need to set up your decoding in order where it decodes the most restrictive numeric type first (Usually `int` first, then `double`). If you are just trying to get a value, you can use a `switch` statement, or use a `if case let` statement. so, something like `switch field { case let .double(value): // Do something with double value case let .integer(value): // Do something with integer value }` http://goshdarnifcaseletsyntax.com/ – Latsu Apr 03 '20 at 19:35
  • i was try this solution. but, i can't get response result from server that was string data if condition data from server was empty – Michael Fernando Sep 02 '21 at 17:14
7

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)"
    }
}
Itai Ferber
  • 28,308
  • 5
  • 77
  • 83
  • Please note that this is valid when implementing a *class* but not with *structs*, I think that you should remove `required` to be valid for structs... – Ahmad F Oct 15 '17 at 19:50
  • @vadian I'm trying this code in my playground, I think there is no need for the `public func`... – Ahmad F Oct 15 '17 at 19:57
  • @AhmadF I just cancelled the (worse) editing, no less no more. – vadian Oct 15 '17 at 20:03
  • @kiwisip and all others trying to edit this answer - don't edit answers to change the original intent of the author! If you disagree: post a comment or post another answer. – Alex Oct 16 '17 at 03:08
  • Thanks for the suggestions — indeed, typed this too hastily. `func` has been removed. – Itai Ferber Oct 16 '17 at 05:22
  • 2
    Although this answer is correct (with a tiny exception), it will work only if my `struct` contains a single property. As soon as I started adding more properties I faced an error `Return from initializer without initializing all stored properties`. I understand now what the error means and maybe I could state that in my question that I will have other properties... I will upvote you nevertheless, but I will accept the other answer just because it is put in the whole context and to reward a new user with some reputation :) Thanks a lot for helping! – errata Oct 16 '17 at 10:20
  • 4
    @errata Er, okay. If you have additional properties, yes, you will need to decode and initialize them as well, just like you would in any other initializer in Swift. – Itai Ferber Oct 16 '17 at 13:43
1

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
AamirR
  • 11,672
  • 4
  • 59
  • 73
1

// Out Put Json

{
   "software_id": "10",
    "name": "Kroll"
},
{
   "software_id": 580,
    "name": "Synmed"
}

// Codable Struct

struct SoftwareDataModel: Codable {

 var softwareId:MyValue?
 var name:String?

 enum CodingKeys: String, CodingKey{
    case softwareId = "software_id"
    case name
 }
}

MYValue is Codable Struct Which help to to convert your datatype into "String" here I mentions only String and Int datatypes.

enum MyValue: Codable {

 case string(String)

 var stringValue: String? {
    switch self {
    case .string(let s):
        return s
    }
 }

init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    if let x = try? container.decode(String.self) {
        self = .string(x)
        return
    }
    if let x = try? container.decode(Int.self) {
        self = .string("\(x)")
        return
    }
    throw DecodingError.typeMismatch(MyValue.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for MyValue"))
}

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

}

// How to get software_id ?

let softwareId =  Struct_object.softwareId?.stringValue ?? "0"
Arjun Patel
  • 1,394
  • 14
  • 23