Home > Enterprise >  Swift Decodable - Is it possible to read part of a tree as a string rather than a data structure?
Swift Decodable - Is it possible to read part of a tree as a string rather than a data structure?

Time:10-27

Here's my problem. Let's say I have a JSON structure that I'm reading using Swift's Codable API. What I want to do is not decode part of the JSON but read it as a string even though it's valid JSON.

In a playground I'm messing about with this code:


import Foundation

let json = #"""
{
    "abc": 123,
    "def": {
        "xyz": "hello world!"
    }
}
"""#

struct X: Decodable {
    let abc: Int
    let def: String

    enum CodingKeys: String, CodingKey {
        case abc
        case def
    }

    init(decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        abc = try container.decode(Int.self, forKey: .abc)

        var defContainer = try container.nestedUnkeyedContainer(forKey: .def)
        def = try defContainer.decode(String.self)
//        def = try container.decode(String.self, forKey: .def)
    }
}

let x = try JSONDecoder().decode(X.self, from: json.data(using: .utf8)!)

Essentially I'm trying to read the def structure as a string instead of a dictionary.

Any clues?

CodePudding user response:

If the resulting string doesn't need to be identical to the corresponding text in the JSON file (i.e. preserve whatever white space is there, etc.), just decode the entire JSON, and then encode the part that you want as a string, and construct a string from the resulting data.

If you do want to preserve exactly the text in the original JSON, including white space, then you'll do better to get that string some other way. Foundation's Scanner class makes it pretty easy to look for some starting token (e.g. "def:") and then read as much data as you want. So consider decoding the JSON in one step, and then separately using a Scanner to dig through the same input data to get the string you need.

CodePudding user response:

Definitely not using JSONDecoder. By the time init(from:) is called, the underlying data has already been thrown away. However you do it, you'll need to parse the JSON yourself. This isn't as hard as it sounds. For example, to extract this string, you could use JSONScanner, which is a few hundred lines of code that you can adjust as you like. With that, you can do things like:

let scanner = JSONScanner()
let string = try scanner.extractData(from: Data(json.utf8), forPath: ["def"])
print(String(data: string, encoding: .utf8)!)

And that will print out:

{
        "xyz": "hello world!"
    }

(Note that RNAJSON is a sandbox framework of mine. It's not a production-ready framework, but it does a lot of interesting JSON things.)

Integrating this into a system that decodes this in a "Decoder-like" way is definitely buildable along these lines, but there's no simple answer down that path. Caleb's suggestion of re-encoding the data into a JSON string is definitely the easiest way.

Using RNAJSON again, there's a type called JSONValue that you can use like this:

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    abc = try container.decode(Int.self, forKey: .abc)

    let defJSON = try container.decode(JSONValue.self, forKey: .def)
    def = String(data: try JSONEncoder().encode(defJSON), encoding: .utf8)!
}

This will make def be a JSON string, but it doesn't promise that key order is maintained or that whitespace is preserved.

  • Related