Codable: Passing data to child decoder

I need to pass some information to a child decoder during deserialization. My issue is best explained with an example:

struct Dog: Decodable {
    let name: String
    let info: DogInfo
    
    private enum CodingKeys: String, CodingKey {
        case name, info, version
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        // DogInfo needs this version to decode itself
        let version = try? container.decode(Int.self, forKey: .version) ?? 1
        
        self.name = try container.decode(String.self, forKey: .name)
        self.info = try container.decode(DogInfo.self, forKey: .info)
    }
}

struct DogInfo: Decodable {
    let tag: Int
    
    private enum CodingKeys: String, CodingKey {
        case tag
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        // How can I get the version number from the parent?
        let version = 2
        let tagAdjustment = (version >= 2 ? 0 : 1)
        self.tag = try container.decode(Int.self, forKey: .tag) + tagAdjustment
    }
}

In version 1 tag was encoded differently and now needs to be adjusted, when decoding the old format.

How can I pass the version information from the parent to the child? I thought of using userInfo on Decoder but that's read-only.

4 Likes

The canonical solution for this would indeed be to pass info between your types through the userInfo dictionary. To properly synchronize access to information between your types, you can pass along a specialized reference type that you yourself define, say

class DogVersion {
    var version: Int?
}

You can then access this in both the parent and the child:

extension CodingUserInfoKey {
    static let dogVersion = CodingUserInfoKey(rawValue: "MyDogVersion")!
}

// Dog
init(from decoder: Decoder) throws {
    // ...
    let version = try container.decodeIfPresent(Int.self, forKey: .version) ?? 1
    if let dogVersion = decoder.userInfo[.dogVersion] as? DogVersion {
        dogVersion.version = version
    } /* else throw an error, potentially */

    // ...
}

// DogInfo
init(from decoder: Decoder) throws {
    let tagAdjustment
    if let version = (decoder.userInfo[.dogVersion] as? DogVersion)?.version {
        tagAdjustment = version >= 2 ? 0 : 1
    } else {
        // Didn't get anything from the parent, or wasn't decoded as part of a Dog
        // Default to tagAdjustment = 1, throw an error, etc.
    }
}

When you go to decode, you set up the storage for this information up-front:

let decoder = JSONDecoder()
decoder.userInfo[.dogVersion] = DogVersion()

let dog = decoder.decode(Dog.self, from: data)

This is a bit verbose, but generalizes well.


If you want, there are some potentially shorter but somewhat messier solutions you can use:

  1. Don't decode DogInfo as its own Codable type, but decode all of its info along with version and create a new DogInfo out of that (this breaks encapsulation of the DogInfo type)

  2. Don't let dogInfo = container.decode(DogInfo.self, forKey: ...) but instead, let dog = DogInfo(from: decoder); sharing a decoder gives DogInfo all of the data that Dog has, and it itself can decode its properties from a subcontainer (this breaks encapsulation of the Dog type)

  3. If the tag adjustment is really as simple as it is here in this code (though I doubt it is), you could always give fileprivate access to the tag and have the parent adjust the tag after the fact (this also breaks encapsulation of the DogInfo type)

  4. You can add a new initializer to DogInfo:

    init(from decoder: Decoder, version: Int) throws {
        // stow the version somewhere, call out to `init(from:)`, and access version from there
    }
    

    You can pass in the appropriate Decoder to the child by doing

    // Dog
    init(from decoder: Decoder) throws {
        // ...
        let  nestedDecoder = container.superDecoder(forKey: .dogInfo)
        let dog = Dog(from: nestedDecoder, version: version)
    }
    

    superDecoder(forKey:) creates a new Decoder which encapsulates the nested container at that key.

I wouldn't recommend the first 3 approaches here as they break encapsulation; approach 4 relies on the definition of superDecoder — this is safe to do, but looks like a strange construction and you'd likely need to document the exact details of how it works.

Depending on how non-trivial the adjustment here is, the approach you take is up to you.

9 Likes

Thanks a lot for your thorough response, @itaiferber. I really appreciate it!

I hadn't thought of storing a reference type in userInfo. I agree that the first 3 alternatives aren't that nice, but I'll look into superDecoder(forKey:).

Always happy to help!

On second thought, that approach might not work as nicely as you won't yet have a place to stash version:

protocol Foo {
    init(with foo: String)
}

struct Bar {
    var foo: String
    var bar: Int?

    init(with foo: String) {
        self.foo = foo
    }

    init(with foo: String, bar: Int) {
        self.bar = bar // error: 'self' used before 'self.init' call or assignment to 'self'
        self.init(with: foo)
    }
}

You can only assign to bar after the delegating call to init(with:):

init(with foo: String, bar: Int) {
    self.init(with: foo)
    self.bar = bar
}

There may be another way to work around this, but it's likely a bit less nice in practice.