TL;DR
Is there a way that I can use JSONDecoder
and write a function which will just read out from given json given field value of specified decodable type?
Imaging I have the following json:
{
"product":{
"name":"PR1",
"price":20
},
"employee":{
"lastName":"Smith",
"department":"IT",
"manager":"Anderson"
}
}
And I have 2 Decodable
structs:
struct Product: Decodable {
var name: String
var price: Int
}
struct Employee: Decodable {
var lastName: String
var department: String
var manager: String
}
I want to write a function
func getValue<T:Decodable>(from json: Data, field: String) -> T { ... }
so that I can call it so:
let product: Product = getValue(from: myJson, field: "product")
let employee: Employee = getValue(from: myJson, field: "employee")
Is this possible with JSONDecoder
or should I mess with JSONSerialization
, first read out the "subtree" of the given json and then pass it to the decoder? Defining structs inside generic functions seems not to be allowed in swift.
Decodable
assumes that you know everything you want at design time to enable static typing. The more dynamic you want, the more creative you have to become. Defining a generic coding keys struct comes really handy in these kind of situations:
/// A structure that holds no fixed key but can generate dynamic keys at run time
struct GenericCodingKeys: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) { self.stringValue = stringValue }
init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
static func makeKey(_ stringValue: String) -> GenericCodingKeys { return self.init(stringValue: stringValue)! }
static func makeKey(_ intValue: Int) -> GenericCodingKeys { return self.init(intValue: intValue)! }
}
/// A structure that retains just the decoder object so we can decode dynamically later
fileprivate struct JSONHelper: Decodable {
let decoder: Decoder
init(from decoder: Decoder) throws {
self.decoder = decoder
}
}
func getValue<T: Decodable>(from json: Data, field: String) throws -> T {
let helper = try JSONDecoder().decode(JSONHelper.self, from: json)
let container = try helper.decoder.container(keyedBy: GenericCodingKeys.self)
return try container.decode(T.self, forKey: .makeKey(field))
}
let product: Product = try getValue(from: json, field: "product")
let employee: Employee = try getValue(from: json, field: "employee")
I would start by saying that Code Different's answer is a viable and good answer, but if you seek a different way of doing it, tho working mostly the same way underneath the surface, I have an alternative solution, using the main components of Code Different's answer, resulting in the code below. One of the main differences, is the fact that one JSONDecoder
is being reused on the same JSON, for each struct
you're extracting, using this.
I would also recommend these:
How to use Any in Codable Type
Swift 4 Codable; How to decode object with single root-level key
/// Conforming to this protocol, makes the type decodable using the JSONContainer class
/// You can use `Decodable` instead.
protocol JSONContainerCodable: Codable {
/// Returns the name that the type is recognized with, in the JSON.
/// This is overridable in types conforming to the protocol.
static var containerIdentifier: String { get }
/// Defines whether or not the type's container identifier is lowercased.
/// Defaults to `true`
static var isLowerCased: Bool { get }
}
extension JSONContainerCodable {
static var containerIdentifier: String {
let identifier = String(describing: self)
return !isLowerCased ? identifier : identifier.lowercased()
}
static var isLowerCased: Bool {
return true
}
}
struct Product: JSONContainerCodable {
var name: String
var price: Int
}
struct Employee: JSONContainerCodable {
var lastName: String
var department: String
var manager: String
}
/// This class is simply a wrapper around JSONDecoder
class JSONContainerDecoder: Decodable {
private struct AnyCodingKeys: CodingKey {
var stringValue: String
var intValue: Int?
init?(intValue: Int) {
self.intValue = intValue
self.stringValue = "\(intValue)"
}
init?(stringValue: String) {
self.stringValue = stringValue
}
init(_ string: String) {
stringValue = string
}
}
private let decoder: JSONDecoder
private let container: KeyedDecodingContainer<AnyCodingKeys>
/// Overrides the initializer as specified in `Decodable`.
required init(from decoder: Decoder) throws {
self.decoder = JSONDecoder()
self.container = try decoder.container(keyedBy: AnyCodingKeys.self)
}
/// Factory initializer. Swift (4.2) currently doesn't support overriding the parentheses operator.
static func decoding(_ data: Data, with decoder: JSONDecoder = JSONDecoder()) throws -> JSONContainerDecoder {
return try decoder.decode(JSONContainerDecoder.self, from: myJSON)
}
/// Gets the given type from the JSON, based on its field/container identifier, and decodes it. Assumes there exists only one type with the given field/container identifier, in the JSON.
func get<T: JSONContainerCodable>(_ type: T.Type, field: String? = nil) throws -> T {
return try container.decode(T.self, forKey: AnyCodingKeys(field ?? T.containerIdentifier))
}
/// Short version of the decode getter above; assumes the variable written to already has its type defined.
func get<T: JSONContainerCodable>(field: String? = nil) throws -> T {
return try get(T.self, field: field)
}
}
let myJSON = """
{
"product": {
"name": "PR1",
"price": 20
},
"employee": {
"lastName": "Smith",
"department": "IT",
"manager": "Anderson"
}
}
""".data(using: .utf8)!
let container = try! JSONContainer.decoding(myJSON)
print(try! container.get( Product.self))
print(try! container.get(Employee.self))
Product(name: "PR1", price: 20)
Employee(lastName: "Smith", department: "IT", manager: "Anderson")
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With