swiftstatedecodecodabledecodable

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?


Solution

  • 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
        }
    }