Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error when use Codable in class inheritance #24

Open
nightwill opened this issue Dec 30, 2024 · 3 comments
Open

Error when use Codable in class inheritance #24

nightwill opened this issue Dec 30, 2024 · 3 comments

Comments

@nightwill
Copy link

I'm trying to use this wonderful library in a class that inherits from the base Codable class.

public override func encode(to encoder: any Encoder) throws {
    try super.encode(to: encoder)
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(id, forKey: .id)
}

And the error appears Multiple calls to container(keyedBy:), unkeyedContainer(), or singleValueContainer()

@nightwill
Copy link
Author

I was able to solve this problem this way. First I added the auxiliary protocol.

private protocol _KeyedEncoder: AnyObject {
    var encodedValues: [HashableKey : EncodableContainer] { get set }
}

extension KeyedEncoder: _KeyedEncoder { }

Then

private func assign<T>(_ valueCreator: @autoclosure () -> T) -> T where T: EncodableContainer {
    // Prevent multiple calls with different containers container(keyedBy:), unkeyedContainer(), or singleValueContainer()
    if let encodedValue = encodedValue as? T {
        return encodedValue
    } else if let previousKeyedContainer = encodedValue as? _KeyedEncoder, T.self is _KeyedEncoder.Type {
        let value = valueCreator()
        (value as! _KeyedEncoder).encodedValues = previousKeyedContainer.encodedValues
        encodedValue = value
        return value
    } else {
        if encodedValue != nil {
            hasMultipleCalls = true
        }
        let value = valueCreator()
        encodedValue = value
        return value
    }
}

And then just remove private for encodedValues in KeyedEncoder

@christophhagen
Copy link
Owner

Interesting. So your goal is to encode values into the same keyed container in both the Superclass and the Subclass?

When I implemented the logic, it seemed reasonable to only one call to a container (single, keyed, or unkeyed).
The codable protocol provides the superEncoder() method, which seemed sufficient for data structures with inheritance.
But I did a few quick tests, and it seems that I was more strict than e.g. JSONDecoder, which allows:

singleValueContainer()

Multiple calls allowed, but only one value can be encoded (-> Exception)

unkeyedContainer()

Multiple calls allowed. All values are encoded sequentially, independent of which container is used to call encode().

keyedContainer()

Multiple calls allowed, even with different CodingKey types. The last value is used when encoding values with the same raw key.

Mixing containers

It's not allowed (Exception) to call any combination of different containers

I'll have to think about the best way to handle this. Some of these "features" can't be integrated directly, since each encoding node manages its own storage. Using multiple containers at the same time requires them to share the same backing storage.

The solution you used works for your case, but once the second call to keyedContainer() is made, the first one is no longer considered:

func encode(to encoder: any Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    var container2 = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(a, forKey: .a) // Not saved
    try container2.encode(b, forKey: .b)
}

I think it's possible to make this work, but it will require larger changes to the code base.

@nightwill
Copy link
Author

Hi Christoph. Yes, that’s true and it’s not an easy task. I did something similar a long time ago, but only for a specialized data format and it’s already difficult for me to remember exactly how it all works. I'll give some code snippets, maybe it will help you somehow. But probably the main idea is to store data by reference and then container and container2 will both write the data (from your example)

private enum RootObject {
    case groupObject(GroupObject) // GroupObject is a reference type
    case dataset(Dataset)
    case attr(Attribute)
    case value(Any)
}

func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey {
        switch rootObject {
        case .groupObject(let groupObject):
            let container = GroupObjectContainer<Key>(groupObject: groupObject, context: context)
            return .init(container)
        case .value(let value):
            guard let dict = value as? [String: Any] else {
                throw "Should be Group Object or Dict"
            }
            let container = DictionaryContainer<Key>(values: dict, context: context)
            return .init(container)
        case .attr(let attribute):
            guard let dict = try attribute.readAny() as? [String: Any] else {
                throw "Attribute should be Compound Type"
            }
            let container = DictionaryContainer<Key>(values: dict, context: context)
            return .init(container)
        default:
            throw "Should be Group Object or Dict"
        }
}

And one more thing, decoding with inheritance also did not work correctly.

And thank you for your work and such a cool package. This should be part of the standard library someday 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants