Skip to content

Instantly share code, notes, and snippets.

@frouo
Last active June 3, 2021 13:21
Show Gist options
  • Select an option

  • Save frouo/64bfdc4fc8c9c111793aa7907ba599f6 to your computer and use it in GitHub Desktop.

Select an option

Save frouo/64bfdc4fc8c9c111793aa7907ba599f6 to your computer and use it in GitHub Desktop.
Decoding JSON array **not** containing the same objects (Swift + Decodable)
let jsonString = """
{
"items": [
{
"id": 20,
"type": "juice",
"fruit": "orange"
},
{
"id": 99,
"type": "soup",
"vegetables": ["carrot", "potatoes", "turnip"]
},
{
"id": 12,
"type": "milkshake",
"composition": {
"milk": "cow",
"fruit": "kiwi"
}
}
]
}
"""
guard let jsonData = jsonString.data(using: .utf8) else { fatalError("Input JSON could not be converted into Data.") }
let jsonDecoded = try JSONDecoder().decode(JSON.self, from: jsonData)
jsonDecoded.items.forEach { (item) in
switch item {
case is Soup: print("🥣 | \(item)")
case is Milkshake: print("🥤 | \(item)")
case is Juice: print("🍹 | \(item)")
default: print("❓ | \(item)")
}
}
//----------------------------//
// PLAYGROUND OUTPUT CONSOLE //
//----------------------------//
//
//🍹 | Item n°20 of type juice. Extra: orange
//🥣 | Item n°99 of type soup. Extra: ["carrot", "potatoes", "turnip"]
//🥤 | Item n°12 of type milkshake. Extra: Composition(milk: "cow", fruit: "kiwi")
//
//------------------------------
/// Allows to decode a JSON with unknown types.
/// Inspiration: https://medium.com/grand-parade/parsing-fields-in-codable-structs-that-can-be-of-any-json-type-e0283d5edb
public enum JSONValue: Decodable {
case string(String)
case int(Int)
case double(Double)
case bool(Bool)
case object([String: JSONValue])
case array([JSONValue])
case nil
var value: Any? {
switch self {
case .string(let value): return value
case .int(let value): return value
case .double(let value): return value
case .bool(let value): return value
case .object(let value): return value.mapValues { $0.value }
case .array(let value): return value.map { $0.value }
case .nil: return nil
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let value = try? container.decode(String.self) {
self = .string(value)
} else if let value = try? container.decode(Int.self) {
self = .int(value)
} else if let value = try? container.decode(Double.self) {
self = .double(value)
} else if let value = try? container.decode(Bool.self) {
self = .bool(value)
} else if let value = try? container.decode([String: JSONValue].self) {
self = .object(value)
} else if let value = try? container.decode([JSONValue].self) {
self = .array(value)
} else if container.decodeNil() {
self = .nil
} else {
throw DecodingError.typeMismatch(JSONValue.self, DecodingError.Context(codingPath: container.codingPath, debugDescription: "Not a JSON."))
}
}
func data() throws -> Data? {
guard let value = value else { return nil }
return try JSONSerialization.data(withJSONObject: value, options: [])
}
}
class Juice: Item {
let fruit: String
private enum CodingKeys: String, CodingKey {
case fruit
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
fruit = try container.decode(String.self, forKey: .fruit)
try super.init(from: decoder)
}
override var debugDescription: String {
return "\(super.debugDescription). Extra: \(fruit)"
}
}
class Soup: Item {
let vegetables: [String]
private enum CodingKeys: String, CodingKey {
case vegetables
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
vegetables = try container.decode([String].self, forKey: .vegetables)
try super.init(from: decoder)
}
override var debugDescription: String {
return "\(super.debugDescription). Extra: \(vegetables)"
}
}
class Milkshake: Item {
let composition: Composition
private enum CodingKeys: String, CodingKey {
case composition
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
composition = try container.decode(Milkshake.Composition.self, forKey: .composition)
try super.init(from: decoder)
}
override var debugDescription: String {
return "\(super.debugDescription). Extra: \(composition)"
}
}
extension Milkshake {
struct Composition: Decodable {
let milk: String
let fruit: String
}
}
class Item: Decodable, CustomDebugStringConvertible {
let id: Int
let type: String
private enum CodingKeys: String, CodingKey {
case id
case type
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
type = try container.decode(String.self, forKey: .type)
}
var debugDescription: String {
return "Item n°\(id) of type \(type)"
}
}
class JSON: Decodable {
let items: [Item]
private enum CodingKeys: String, CodingKey {
case items
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let values = try container.decode([JSONValue].self, forKey: .items)
items = try values.map { value -> Item in
guard let data = try value.data()
else { throw DecodingError.dataCorruptedError(forKey: .items, in: container, debugDescription: "Cannot convert \(value) into Data.") }
if let juice = try? JSONDecoder().decode(Juice.self, from: data) {
return juice
} else if let soup = try? JSONDecoder().decode(Soup.self, from: data) {
return soup
} else if let milkshake = try? JSONDecoder().decode(Milkshake.self, from: data) {
return milkshake
} else {
throw DecodingError.dataCorruptedError(forKey: .items, in: container, debugDescription: "Unknown decoder for data: \(String(data: data, encoding: .utf8) ?? "?_?").")
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment