Parse JSON Array that contains string and int in Swift 4 - json

I have a simple array
Array[4][
"A",
1,
"A1",
13
]
But how can i parse an JSON array when it contains both int and string? I have no problem converting all the values inside to string if they aren't already, but i can't find any function to do this.
Thanks.

You are getting Array of elements as String or Int which resembles with Array of enum type. So you can parse the underlying Type with the help of an enum.
Have the underlying type structured as:
enum StringOrIntType: Codable {
case string(String)
case int(Int)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
self = try .string(container.decode(String.self))
} catch DecodingError.typeMismatch {
do {
self = try .int(container.decode(Int.self))
} catch DecodingError.typeMismatch {
throw DecodingError.typeMismatch(StringOrIntType.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Encoded payload not of an expected type"))
}
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .int(let int):
try container.encode(int)
case .string(let string):
try container.encode(string)
}
}
}
Decoding process:
let jsonData = """
["A", 1, "A1", 13, 15, 2, "B"]
""".data(using: .utf8)!
do {
let decodedArray = try JSONDecoder().decode([StringOrIntType].self, from:jsonData)
// Here, you have your Array
print(decodedArray) // [.string("A"), .int(1), .string("A1"), .int(13), .int(15), .int(2), .string("B")]
// If you want to get elements from this Array, you might do something like below
decodedArray.forEach({ (value) in
if case .string(let integer) = value {
print(integer) // "A", "A1", "B"
}
if case .int(let int) = value {
print(int) // 1, 13, 15, 2
}
})
} catch {
print(error)
}
From the comment on accepted answer: You don't need to worry about the ordering of the items anymore.

Complicated way with Codable
Decode the array with unkeyedContainer
Use a while loop with condition !isAtEnd and decode Int in a do - catch block. If it fails decode String
Easy way with traditional JSONSerialization
Deserialize the object to [CustomStringConvertible] and map the array to [String] with "\($0)"
Edit:
This is an example how to decode the array with Decodable if the items are pairs and the type order is the same:
let jsonArray = """
["A", 1, "A1", 13]
"""
struct Item : Decodable {
var array = [String]()
init(from decoder: Decoder) throws {
var arrayContainer = try decoder.unkeyedContainer()
while !arrayContainer.isAtEnd {
let string = try arrayContainer.decode(String.self)
let int = try arrayContainer.decode(Int.self)
array.append(String(int))
array.append(string)
}
}
}
let data = Data(jsonArray.utf8)
do {
let result = try JSONDecoder().decode(Item.self, from: data)
print(result.array)
} catch { print(error) }

Pasting the JSON sample [1, "1"] into quicktype gives the following Codable implementation:
typealias IntOrStrings = [IntOrString]
enum IntOrString: Codable {
case integer(Int)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(Int.self) {
self = .integer(x)
return
}
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
throw DecodingError.typeMismatch(IntOrString.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for IntOrString"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .integer(let x):
try container.encode(x)
case .string(let x):
try container.encode(x)
}
}
}
Here is the full source that allows you to do the following:
let items = try IntStrings("[1, \"1\"]")
// Now you have:
// items == [.integer(1), .string("1")]
// so you can map or iterate over this
This is the most typesafe way to represent an int-or-string array from JSON.

Related

How can I decode a JSON array with multiple data types?

I'm trying to decode a JSON file from an API that I want to use but the value array contains a bunch of strings and an int at the end. When I specify the data type in the struct as AnyObject, it says that the struct does not conform to the Decodable protocol. Am I missing something? Is there a way I can fetch the data without the last Int?
You can use QuickType to parse the model data from JSON.
// MARK: - DataModel
struct DataModel: Codable {
let title: String
let blanks: [String]
let value: [Value]
}
enum Value: Codable {
case integer(Int)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(Int.self) {
self = .integer(x)
return
}
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
throw DecodingError.typeMismatch(Value.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for Value"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .integer(let x):
try container.encode(x)
case .string(let x):
try container.encode(x)
}
}
}
Please use the following code to check the value type of Value below.
let jsonData = jsonString.data(using: .utf8)!
let dataModel = try? JSONDecoder().decode(DataModel.self, from: jsonData)
dataModel?.value.forEach { value in
switch value {
case .integer(let intValue):
print(intValue)
case .string(let stringValue):
print(stringValue)
}
}
Decodable expects concrete types which conform to the protocol. Any(Object) is not supported.
You could decode Value as UnkeyedContainer manually. The result is the string array in strings and the integer value in integer.
struct DataModel: Decodable {
let title: String
let blanks: [String]
let value: Value
}
struct Value: Decodable {
let strings : [String]
let integer : Int
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var stringData = [String]()
guard let numberOfItems = container.count else {
throw DecodingError.dataCorruptedError(in: container,
debugDescription: "Number of items in the array is unknown")
}
while container.currentIndex < numberOfItems - 1 {
stringData.append(try container.decode(String.self))
}
strings = stringData
integer = try container.decode(Int.self)
}
}
Side note: A JSON value is never an object (reference type).

In Swift to safely access JSON Dictionary with arbitrary structure of keys and values?

I am attempting to read keys and values out of a JSON object that does not have a defined structure. The JSON looks similar to:
{
"content":"me,menu_cta,page",
"me": {
"email": "person#example.com",
"first_name": "Jordan"
},
"menu_cta": {
"menu_text": "Tap here"
},
"page": {
"how_it_works": "Make sure you're tapping the right spots.'",
"page_icon": "https://www.example.com/button.png",
"terms": "Terms and Conditions"
}
}
I don't think I can use Codeable since I'm unsure at compile-time what the keys will be inside the Dictionary and I'm also unsure if the values will be Strings or Dictionaries. The keys in the JSON that are read are dynamic based on user interaction.
I can start by setting up the Dictionary object:
let json = try? JSONSerialization.jsonObject(with: data, options: [])
if let dictionary = json as? [String: Any] {
self.prefetchDictionary = content
}
}
But when trying to read any data, ideally I would like to ask for page.how_it_works but I think the best method for doing this looks like this:
if let pageGenericDictionary = prefetchDictionary?["page"] {
if let pageDictionary = prefetchDictionary as? [String:String] {
headerText.text = pageDictionary["how_it_works"]
}
}
Is this the easiest way? Is there any method to write a simple function that could traverse down easily with definitions like page.how_it_works or me.email?
I am trying to avoid including a 3rd party library to achieve this.
First, how arbitrary is it? The above suggests that this is exactly [String: [String: String]], which is Decodable, and you should use JSONDecoder rather than JSONSerialization (you should generally avoid JSONSerialization):
let dict = try JSONDecoder().decode([String: [String: String]].self, from: json)
If the actual structure is arbitrary, then I suggest an arbitrary JSON decoder. Simplified version here (full version):
enum JSON: Codable {
struct Key: CodingKey, Hashable {
let stringValue: String
init(_ string: String) { self.stringValue = string }
init?(stringValue: String) { self.init(stringValue) }
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
case string(String)
case number(Double) // FIXME: Split Int and Double
case object([Key: JSON])
case array([JSON])
case bool(Bool)
case null
init(from decoder: Decoder) throws {
if let string = try? decoder.singleValueContainer().decode(String.self) { self = .string(string) }
else if let number = try? decoder.singleValueContainer().decode(Double.self) { self = .number(number) }
else if let object = try? decoder.container(keyedBy: Key.self) {
var result: [Key: JSON] = [:]
for key in object.allKeys {
result[key] = (try? object.decode(JSON.self, forKey: key)) ?? .null
}
self = .object(result)
}
else if var array = try? decoder.unkeyedContainer() {
var result: [JSON] = []
for _ in 0..<(array.count ?? 0) {
result.append(try array.decode(JSON.self))
}
self = .array(result)
}
else if let bool = try? decoder.singleValueContainer().decode(Bool.self) { self = .bool(bool) }
else if let isNull = try? decoder.singleValueContainer().decodeNil(), isNull { self = .null }
else { throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [],
debugDescription: "Unknown JSON type")) }
}
func encode(to encoder: Encoder) throws {
switch self {
case .string(let string):
var container = encoder.singleValueContainer()
try container.encode(string)
case .number(let number):
var container = encoder.singleValueContainer()
try container.encode(number)
case .bool(let bool):
var container = encoder.singleValueContainer()
try container.encode(bool)
case .object(let object):
var container = encoder.container(keyedBy: Key.self)
for (key, value) in object {
try container.encode(value, forKey: key)
}
case .array(let array):
var container = encoder.unkeyedContainer()
for value in array {
try container.encode(value)
}
case .null:
var container = encoder.singleValueContainer()
try container.encodeNil()
}
}
subscript(key: String) -> JSON? {
guard let jsonKey = Key(stringValue: key),
case .object(let object) = self,
let value = object[jsonKey]
else { return nil }
return value
}
var stringValue: String? {
switch self {
case .string(let string): return string
default: return nil
}
}
var doubleValue: Double? {
switch self {
case .number(let number): return number
default: return nil
}
}
var intValue: Int? {
switch self {
case .number(let number): return Int(number)
default: return nil
}
}
subscript(index: Int) -> JSON? {
switch self {
case .array(let array): return array[index]
default: return nil
}
}
var boolValue: Bool? {
switch self {
case .bool(let bool): return bool
default: return nil
}
}
}
With that, you get the syntax:
let result = try JSONDecoder().decode(JSON.self, from: json)
let str: String? = result["page"]?["how_it_works"]?.stringValue
If you really want a dot-style syntax, you can get that with #dynamicMemberLookup:
#dynamicMemberLookup
enum JSON: Codable {
subscript(dynamicMember member: String) -> JSON {
return self[member] ?? .null
}
... the rest is the same ...
That would give you the syntax:
let x = result.page.how_it_works.stringValue

Swift: Print variable with different types (enum) from JSON in for-loop

The situation is like this:
How to decode a JSON property with different types?
I used the code which is marked as solution in the linked question and I want to print a variable in a for loop:
struct GetEvents: Decodable{
var id: String?
var expansion: String?
var distance: Distance?
}
enum Distance: Codable {
case int(Int)
case string(String)
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .int(let v): try container.encode(v)
case .string(let v): try container.encode(v)
}
}
init(from decoder: Decoder) throws {
let value = try decoder.singleValueContainer()
do {
self = .int(try value.decode(Int.self))
} catch DecodingError.typeMismatch {
self = .string(try value.decode(String.self))
}
}
enum ParseError: Error {
case notRecognizedType(Any)
}
}
I try to print all variables with a for-loop because i have multiple objects inside the object.
for i in 0...(getEventsText.items.count - 1) {
if let idAsString = getEventsText.items[i].id {
print(idAsString)
}
if let distanceAsString = getEventsText.items[i].distance {
print(distanceAsString)
}
if let epansionAsString = getEventsText.items[i].offer_expansion {
print(expansionAsString)
}
I get the id and the expansion but the distance is showing as int(-1) and not as -1
If I just output it like this:
for x in range getEventsText.items {
print(x)
}
It gives back
GetEvents(id: Optional("7576"), distance: Swift.ImplicitlyUnwrappedOptional.some(DB_Read_enums_Test1.Distance.int(0)), expansion: Optional("0"))
Let Distance implement CustomStringConvertible and add the following
var description: String {
switch self {
case let .int(value):
return "\(value)"
case let .string(value):
return value
}
}
enum Distance: Codable, CustomStringConvertible {
case int(Int)
case string(String)
var description: String {
switch self {
case let .int(value):
return "\(value)"
case let .string(value):
return value
}
//rest of code
}
Test case
let data = """
{"id": "7576", "expansion": "0", "distance": -1}
""".data(using: .utf8)!
do {
let decoder = JSONDecoder()
let result = try decoder.decode(GetEvents.self, from: data)
print(String(describing: result.id))
print(String(describing: result.expansion))
print(String(describing: result.distance))
} catch {
print(error)
}
output is
Optional("7576")
Optional("0")
Optional(-1)
and if I change my json to "..."distance": "long"} then the last output row changes to
Optional(long)

How to deal with completely dynamic JSON responses

Maybe someone in the community has had similar struggles and have come up with a workable solution.
We're currently working on a polyglot key/value store. Given this, we'll generally have no knowledge of what will be stored ahead of time.
Consider the following struct
struct Character : Codable, Equatable {
let name: String
let age: Int
let gender: Gender
let hobbies: [String]
static func ==(lhs: Character, rhs: Character) -> Bool {
return (lhs.name == rhs.name
&& lhs.age == rhs.age
&& lhs.gender == rhs.gender
&& lhs.hobbies == rhs.hobbies)
}
}
When sending/receiving Character entities over the wire, everything is fairly straight forward. The user can provide us the Type in which we can decode into.
However, we do have the ability to dynamically query the entities stored within the backend. For example, we can request the value of the 'name' property and have that returned.
This dynamism is a pain point. In addition to not knowing the type of the properties outside of the fact that they are Codable, the format that is returned can be dynamic as well.
Here's some examples of response for two different calls extracting properties:
{"value":"Bilbo"}
and
{"value":["[Ljava.lang.Object;",["Bilbo",111]]}
In some cases, it could be an equivalent of a dictionary.
Right now, I have the following structs for dealing with responses:
fileprivate struct ScalarValue<T: Decodable> : Decodable {
var value: T?
}
Using the Character example, the type passed to the decoder would be:
ScalarValue<Character>.self
However, for the single value, array, or dictionary case, I'm somewhat stuck.
I've started with something like:
fileprivate struct AnyDecodable: Decodable {
init(from decoder: Decoder) throws {
// ???
}
}
Based on the possible return types I've described above, I'm not sure if this is possible with the current API.
Thoughts?
Swift can definitely handle an arbitrary JSON decodable. This isn't the same thing as an arbitrary decodable. JSON can't encode all possible values. But this structure will decode anything that can be expressed in JSON, and from there you can explore it in a type-safe way without resorting to dangerous and awkward tools like Any.
enum JSON: Decodable, CustomStringConvertible {
var description: String {
switch self {
case .string(let string): return "\"\(string)\""
case .number(let double):
if let int = Int(exactly: double) {
return "\(int)"
} else {
return "\(double)"
}
case .object(let object):
return "\(object)"
case .array(let array):
return "\(array)"
case .bool(let bool):
return "\(bool)"
case .null:
return "null"
}
}
var isEmpty: Bool {
switch self {
case .string(let string): return string.isEmpty
case .object(let object): return object.isEmpty
case .array(let array): return array.isEmpty
case .null: return true
case .number, .bool: return false
}
}
struct Key: CodingKey, Hashable, CustomStringConvertible {
var description: String {
return stringValue
}
var hashValue: Int { return stringValue.hash }
static func ==(lhs: JSON.Key, rhs: JSON.Key) -> Bool {
return lhs.stringValue == rhs.stringValue
}
let stringValue: String
init(_ string: String) { self.stringValue = string }
init?(stringValue: String) { self.init(stringValue) }
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
case string(String)
case number(Double) // FIXME: Split Int and Double
case object([Key: JSON])
case array([JSON])
case bool(Bool)
case null
init(from decoder: Decoder) throws {
if let string = try? decoder.singleValueContainer().decode(String.self) { self = .string(string) }
else if let number = try? decoder.singleValueContainer().decode(Double.self) { self = .number(number) }
else if let object = try? decoder.container(keyedBy: Key.self) {
var result: [Key: JSON] = [:]
for key in object.allKeys {
result[key] = (try? object.decode(JSON.self, forKey: key)) ?? .null
}
self = .object(result)
}
else if var array = try? decoder.unkeyedContainer() {
var result: [JSON] = []
for _ in 0..<(array.count ?? 0) {
result.append(try array.decode(JSON.self))
}
self = .array(result)
}
else if let bool = try? decoder.singleValueContainer().decode(Bool.self) { self = .bool(bool) }
else {
self = .null
}
}
var objectValue: [String: JSON]? {
switch self {
case .object(let object):
let mapped: [String: JSON] = Dictionary(uniqueKeysWithValues:
object.map { (key, value) in (key.stringValue, value) })
return mapped
default: return nil
}
}
var arrayValue: [JSON]? {
switch self {
case .array(let array): return array
default: return nil
}
}
subscript(key: String) -> JSON? {
guard let jsonKey = Key(stringValue: key),
case .object(let object) = self,
let value = object[jsonKey]
else { return nil }
return value
}
var stringValue: String? {
switch self {
case .string(let string): return string
default: return nil
}
}
var doubleValue: Double? {
switch self {
case .number(let number): return number
default: return nil
}
}
var intValue: Int? {
switch self {
case .number(let number): return Int(number)
default: return nil
}
}
subscript(index: Int) -> JSON? {
switch self {
case .array(let array): return array[index]
default: return nil
}
}
var boolValue: Bool? {
switch self {
case .bool(let bool): return bool
default: return nil
}
}
}
With this, you can do things like:
let bilboJSON = """
{"value":"Bilbo"}
""".data(using: .utf8)!
let bilbo = try! JSONDecoder().decode(JSON.self, from: bilboJSON)
bilbo["value"] // "Bilbo"
let javaJSON = """
{"value":["[Ljava.lang.Object;",["Bilbo",111]]}
""".data(using: .utf8)!
let java = try! JSONDecoder().decode(JSON.self, from: javaJSON)
java["value"]?[1] // ["Bilbo", 111]
java["value"]?[1]?[0]?.stringValue // "Bilbo" (as a String rather than a JSON.string)
The proliferation of ? is somewhat ugly, but using throws on this doesn't really make the interface much nicer in my experiments (particularly because subscripts can't throw). Some tweaking may be advisable based on your particular use cases.
I wrote an AnyCodable struct myself for this purpose:
struct AnyCodable: Decodable {
var value: Any
struct CodingKeys: CodingKey {
var stringValue: String
var intValue: Int?
init?(intValue: Int) {
self.stringValue = "\(intValue)"
self.intValue = intValue
}
init?(stringValue: String) { self.stringValue = stringValue }
}
init(value: Any) {
self.value = value
}
init(from decoder: Decoder) throws {
if let container = try? decoder.container(keyedBy: CodingKeys.self) {
var result = [String: Any]()
try container.allKeys.forEach { (key) throws in
result[key.stringValue] = try container.decode(AnyCodable.self, forKey: key).value
}
value = result
} else if var container = try? decoder.unkeyedContainer() {
var result = [Any]()
while !container.isAtEnd {
result.append(try container.decode(AnyCodable.self).value)
}
value = result
} else if let container = try? decoder.singleValueContainer() {
if let intVal = try? container.decode(Int.self) {
value = intVal
} else if let doubleVal = try? container.decode(Double.self) {
value = doubleVal
} else if let boolVal = try? container.decode(Bool.self) {
value = boolVal
} else if let stringVal = try? container.decode(String.self) {
value = stringVal
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "the container contains nothing serialisable")
}
} else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not serialise"))
}
}
}
extension AnyCodable: Encodable {
func encode(to encoder: Encoder) throws {
if let array = value as? [Any] {
var container = encoder.unkeyedContainer()
for value in array {
let decodable = AnyCodable(value: value)
try container.encode(decodable)
}
} else if let dictionary = value as? [String: Any] {
var container = encoder.container(keyedBy: CodingKeys.self)
for (key, value) in dictionary {
let codingKey = CodingKeys(stringValue: key)!
let decodable = AnyCodable(value: value)
try container.encode(decodable, forKey: codingKey)
}
} else {
var container = encoder.singleValueContainer()
if let intVal = value as? Int {
try container.encode(intVal)
} else if let doubleVal = value as? Double {
try container.encode(doubleVal)
} else if let boolVal = value as? Bool {
try container.encode(boolVal)
} else if let stringVal = value as? String {
try container.encode(stringVal)
} else {
throw EncodingError.invalidValue(value, EncodingError.Context.init(codingPath: [], debugDescription: "The value is not encodable"))
}
}
}
}
It works with nested dictionaries/arrays too. You can try it with any json in a playground.
let decoded = try! JSONDecoder().decode(AnyCodable.self, from: jsonData)
Yes, is possible to achieve what you described via the existing Codable API, and in an elegant manner I'd say (though I might be subjective here since I'm talking about my code :) ).
Let try to figure out what is needed for this task:
First things first, you need to declare all properties as optional. This is needed as the decoder is likely to have to deal with partial responses.
struct Character: Codable {
let name: String?
let age: Int?
let hobbies: [String]?
}
Next, we need a way to figure out how to map the struct properties to the various fields from the partial JSONs. Luckily the Codable API can help us here via the CodingKeys enum:
enum CodingKeys: String, CodingKey {
case name
case age
case hobbies
}
The first tricky part is to somehow convert the CodingKeys enum into an array of strings, that we can use for the array response - {"value":["[Ljava.lang.Object;",["Bilbo",111]]}. We are in luck here, there are various sources on the internet and SO that address the question of getting all cases of an enum. My preferred solutions is the RawRepresentable extension, since CodingKey is raw representable and it's raw value is a String:
// Adds support for retrieving all enum cases. Since we refer a protocol here,
// theoretically this method can be called on other types than enum
public extension RawRepresentable {
static var enumCases: [Self] {
var caseIndex: Int = 0
return Array(AnyIterator {
defer { caseIndex += 1 }
return withUnsafePointer(to: &caseIndex) {
$0.withMemoryRebound(to: Self.self, capacity: 1) { $0.pointee }
}
})
}
}
We're almost there, but we need some more work before we can decode.
Now that we have a Decodable type, a list of coding keys to use, we need a decoder that makes use of these. But before that, we need to be able to recognise types that can be partially decoded. Let's add a new protocol
protocol PartiallyDecodable: Decodable {
associatedtype PartialKeys: RawRepresentable
}
and make Character conform to it
struct Character : Codable, PartiallyDecodable {
typealias PartialKeys = CodingKeys
The finishing piece is the decoding part. We can reuse the JSONDecoder that comes with the standard library:
// Tells the form of data the server sent and we want to decode:
enum PartialDecodingStrategy {
case singleKey(String)
case arrayOfValues
case dictionary
}
extension JSONDecoder {
// Decodes an object by using a decoding strategy
func partialDecode<T>(_ type: T.Type, withStrategy strategy: PartialDecodingStrategy, from data: Data) throws -> T where T : PartiallyDecodable, T.PartialKeys.RawValue == String {
Connecting all of the above results in the following infrastructure:
// Adds support for retrieving all enum cases. Since we refer a protocol here,
// theoretically this method can be called on other types than enum
public extension RawRepresentable {
static var enumCases: [Self] {
var caseIndex: Int = 0
return Array(AnyIterator {
defer { caseIndex += 1 }
return withUnsafePointer(to: &caseIndex) {
$0.withMemoryRebound(to: Self.self, capacity: 1) { $0.pointee }
}
})
}
}
protocol PartiallyDecodable: Decodable {
associatedtype PartialKeys: RawRepresentable
}
// Tells the form of data the server sent and we want to decode:
enum PartialDecodingStrategy {
case singleKey(String)
case arrayOfValues
case dictionary
}
extension JSONDecoder {
// Decodes an object by using a decoding strategy
func partialDecode<T>(_ type: T.Type, withStrategy strategy: PartialDecodingStrategy, from data: Data) throws -> T where T : PartiallyDecodable, T.PartialKeys.RawValue == String {
guard let partialJSON = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [AnyHashable:Any] else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Invalid JSON"))
}
guard let value = partialJSON["value"] else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Missing \"value\" key"))
}
let processedJSON: [AnyHashable:Any]
switch strategy {
case let .singleKey(key):
processedJSON = [key:value]
case .arrayOfValues:
guard let values = value as? [Any],
values.count == 2,
let properties = values[1] as? [Any] else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Invalid JSON: expected a 2 elements array for the \"value\" key"))
}
processedJSON = zip(T.PartialKeys.enumCases, properties)
.reduce(into: [:]) { $0[$1.0.rawValue] = $1.1 }
case .dictionary:
guard let dict = value as? [AnyHashable:Any] else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Invalid JSON: expected a dictionary for the \"value\" key"))
}
processedJSON = dict
}
return try decode(type, from: JSONSerialization.data(withJSONObject: processedJSON, options: []))
}
}
We want to be able to partially decode Character, so we make it adopt all the required protocols:
struct Character: Codable, PartiallyDecodable {
typealias PartialKeys = CodingKeys
let name: String?
let age: Int?
let hobbies: [String]?
enum CodingKeys: String, CodingKey {
case name
case age
case hobbies
}
}
Now the fun part, let's test it:
let decoder = JSONDecoder()
let jsonData1 = "{\"value\":\"Bilbo\"}".data(using: .utf8)!
print((try? decoder.partialDecode(Character.self,
withStrategy: .singleKey(Character.CodingKeys.name.rawValue),
from: jsonData1)) as Any)
let jsonData2 = "{\"value\":[\"[Ljava.lang.Object;\",[\"Bilbo\",111]]}".data(using: .utf8)!
print((try? decoder.partialDecode(Character.self,
withStrategy: .arrayOfValues,
from: jsonData2)) as Any)
let jsonData3 = "{\"value\":{\"name\":\"Bilbo\",\"age\":111,\"hobbies\":[\"rings\"]}}".data(using: .utf8)!
print((try? decoder.partialDecode(Character.self,
withStrategy: .dictionary,
from: jsonData3)) as Any)
As we might expect, the output is the following:
Optional(MyApp.Character(name: Optional("Bilbo"), age: nil, hobbies: nil))
Optional(MyApp.Character(name: Optional("Bilbo"), age: Optional(111), hobbies: nil))
Optional(MyApp.Character(name: Optional("Bilbo"), age: Optional(111), hobbies: Optional(["rings"])))
As we can see, with the proper infrastructure laid out, the only requirements for a type to be partially decodable is to conform to PartiallyDecodable and to have an enum that says which keys to decode. These requirements are easy to be followed.

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?
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)
}
}
}
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)"
}
}
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
// 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"