This is likely too late for the OP, but I had a similar problem that needed a solution.
Hopefully this is useful for others.
For my problem, I wanted to use Decodable
to decode my JSON
with Swift 5.1. However at various points in my object hierarchy, I wanted to return an objC object (from a third party library) that did not support Decodable
, but did support decoding from a (non-trivial) JSON string. I solved the problem by using JSONSerialization
to create an untyped object hierarchy I could retrieve from the decoder's userInfo
property and search with the decoder's contextPath
to find my data, and then use JSONSerialization
to
convert it back to string data.
This solution makes no assumption about the object/array hierarchy required to get to the object that has the "data" key.
// Swift 5.1 Playground
import Foundation
// Input configuration JSON
let jsonStr = """
{
"foo":"bar",
"bars": [
{
"data":{
"thing1":"#111100",
"thing2":12
}
},
{
"data":{
"thing1":"#000011",
"thing2":64.125
}
}
]
}
"""
// Object passed to the decoder in the UserInfo Dictionary
// This will contain the serialized JSON data for use by
// child objects
struct MyCodingOptions {
let json: Any
static let key = CodingUserInfoKey(rawValue: "com.unique.mycodingoptions")!
}
let jsonData = Data(jsonStr.utf8)
let json = try JSONSerialization.jsonObject(with: jsonData)
let options = MyCodingOptions(json: json)
let decoder = JSONDecoder()
decoder.userInfo = [MyCodingOptions.key: options]
// My object hierarchy
struct Root: Decodable {
let foo: String
let bars: [Bar]
}
struct Bar: Decodable {
let data: Data?
enum CodingKeys: String, CodingKey {
case data = "data"
}
}
// Implement a custom decoder for Bar
// Use the context path and the serialized JSON to get the json value
// of "data" and then deserialize it back to data.
extension Bar {
init(from decoder: Decoder) throws {
var data: Data? = nil
if let options = decoder.userInfo[MyCodingOptions.key] as? MyCodingOptions {
// intialize item to the whole json object, then mutate it down the "path" to Bar
var item: Any? = options.json
let path = decoder.codingPath // The path to the current object, does not include self
for key in path {
if let intKey = key.intValue {
//array
item = (item as? [Any])?[intKey]
} else {
//object
item = (item as? [String:Any])?[key.stringValue]
}
}
// item is now Bar, which is an object (Dictionary)
let bar = item as? [String:Any]
let dataKey = CodingKeys.data.rawValue
if let item = bar?[dataKey] {
data = try JSONSerialization.data(withJSONObject: item)
}
}
self.init(data: data)
}
}
if let root = try? decoder.decode(Root.self, from: jsonData) {
print("foo: \(root.foo)")
for (i, bar) in root.bars.enumerated() {
if let data = bar.data {
print("data #\(i): \(String(decoding: data, as: UTF8.self))")
}
}
}
//prints:
// foo: bar
// data #0: {"thing2":12,"thing1":"#111100"}
// data #1: {"thing2":64.125,"thing1":"#000011"}
Data
. Regardless of whether its dict, array, number or string. – Seaddon”data"
key, no matter the format? If so, looks tricky... ;) – Olson