Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can you pass additional state data through to a (possibly custom) JSONDecoder as part of the Decode operation?

We have a tree-based data model that is based around the Codable protocol. The root of the tree holds its immediate children, as well as a reference to all children in the hierarchy, as seen here...

Root
 |
 |-->Children
 |   |
 |   |-->Item 1
 |   |-->Item 2
 |   |   |
 |   |   \-->Children
 |   |       |
 |   |       \-->Item 3
 |   |
 |   \-->Item 4
 |       |
 |       \-->Children
 |           |
 |           \-->Item 5
 |               |
 |               \-->Children
 |                   |
 |                   \-->Item 6
 |
 \-->AllChildren  <<-- Not Serialized!!
      |
      |-->Item 1
      |-->Item 2
      |-->Item 3
      |-->Item 4
      |-->Item 5
      \-->Item 6

Now the AllChildren part isn't serialized as they are just references to the actual instances from above.

To make the above work, we need to populate AllChildren programmatically, as those children are being decoded. However, we aren't sure how to pass the Root object into the children's init(from:Decoder) calls to handle that as we don't see any way to pass additional state data into the decoding chain. It seems the only information you have available is the Decoder, which you don't control.

Our work-around is inside the init(from:Decoder) of the Root, once it's done decoding/initializing all of its children, it then re-crawls the entire hierarchy, slurping up the children, but I really hate that I have to re-crawl the hierarchy after just doing it during the init(from:Decoder) pass.

So is there a way to pass additional state information into the Decode process, preferably as something on the Decoder handed to the init(from:Decoder) calls?

like image 406
Mark A. Donohoe Avatar asked Sep 17 '25 16:09

Mark A. Donohoe


2 Answers

You can assign an arbitrary set of key/value pairs to the decoder's userInfo and read them in the init methods.

Note that the keys to userInfo are CodingUserInfoKey, but you can create those from strings using CodingUserInfoKey(rawValue: "key")!.


EDIT: After a few years, I wanted to update this with a bit better solution, which is to create a custom init that accepts a Decoder and use that to pass along state. (You hint at this in the comments, and it really is as easy as that.)

I haven't tested this carefully, and I'm not quite certain the JSON you're using, but I think this approach should make how to adapt it clear.

// This works for either structs or classes
struct Item {
    var value: Int
    var children: [Item]

    enum CodingKeys: CodingKey { case value, children }

    // This is not the normal Decodable. In fact, Item is not Decodable at all
    // It's just an init that takes extra state
    init(from decoder: Decoder, allChildren: inout [Item]) throws {
        // Decode the normal stuff
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.value = try container.decode(Int.self, forKey: .value)

        // Decode children by hand
        let childrenContainer = try container.nestedUnkeyedContainer(forKey: .children)

        var children: [Item] = []
        while !childrenContainer.isAtEnd {
            // For each child, pass along the `allChildren` array
            children.append(try Item(from: decoder, allChildren: &allChildren))
        }
        self.children = children

        // Append self to the list. This creates a depth first search order.
        // Item in this case is a struct, so this is a copy. If Item were a class
        // then this would be a reference.
        allChildren.append(self)
    }
}

struct Root: Decodable {
    var children: [Item]
    var allChildren: [Item]

    // Basically the same as Item, but creates the `allChildren` first.
    init(from decoder: Decoder) throws {
        var allChildren: [Item] = []

        let childrenContainer = try decoder.unkeyedContainer()

        var children: [Item] = []
        while !childrenContainer.isAtEnd {
            children.append(try Item(from: decoder, allChildren: &allChildren))
        }

        self.children = children
        self.allChildren = allChildren
    }
}
like image 189
Rob Napier Avatar answered Sep 20 '25 04:09

Rob Napier


As of iOS 17 you can now use DecodableWithConfiguration instead of Decodable. This will allow you to pass an object through to the decodable init like so:

struct DecodingInfo {
    let rootObject: [String: Any]
}

struct Tokens: DecodableWithConfiguration {
        
    init(from decoder: Decoder, configuration: DecodingInfo) throws {
        let root = configuration.rootObject
        // ...
    }
}

// ...

let tokens = try JSONDecoder().decode(Tokens.self, from: data, configuration: DecodingInfo(rootObject: jsonObject))

In my use case I decoded the raw json object to a [String: Any] to pass into the decoding process so that i could traverse the dictionary based on values I found in the json that would be references to other parts of the json

let jsonObject = try JSONSerialization.jsonObject(with: jsonString.data(using: .utf8)!, options: []) as? [String: [String: Any]]
like image 40
Fonix Avatar answered Sep 20 '25 06:09

Fonix