# Codable items with identity?

**URL:** <https://forums.swift.org/t/codable-items-with-identity/60726>\
**Category:** Using Swift\
**Created:** [October 8, 2022, 2:36am UTC](https://forums.swift.org/t/codable-items-with-identity/60726 "2022-10-08T02:36:26Z")\
**Posts on this page:** 1\
**Showing post:** 3

<div class="post-metadata">

**Author:** ![gnuoyd](https://avatars.discourse-cdn.com/v4/letter/g/bc8723/32.png) [@gnuoyd](https://forums.swift.org/u/gnuoyd)\
**Post date:** [October 13, 2022, 9:22pm UTC](https://forums.swift.org/t/codable-items-with-identity/60726/3 "2022-10-13T21:22:02Z")

</div>

> In a word, no. This has been an unresolved design issue for several years.
> 
> Currently, there is nothing that will encode references instead of values, although it does seem feasible to write a new encoder type that will encode references.

Thanks for confirming the unresolved issue.

I wrote some untested but compilable code, below, that may help solve  
the problem in a pinch, but there are some problems. I haven't dealt  
with object-graph cycles except to throw if one occurs during decoding.  
I assume that a Decoder will decode objects in the same order as the  
Encoder encoded them, but that's not founded on any promise I have seen  
in the documentation.

I provide a protocol, CodableWithIdentity, that a class conforms to by  
providing both an initializer and an encode function that operate on  
`Keyed{De,En}codingContainer`.

CodableWithIdentity relies on the user to stash an `ObjectRegistry`  
in the `userInfo` property of the `Encoder` or `Decoder`. The  
`ObjectRegistry` keeps a mapping from UInt64 to AnyObject and back.  
Here is the code.

```swift
extension CodingUserInfoKey {
        public static let objectRegistry =
            CodingUserInfoKey(rawValue: "com.whitecoralislands.objectRegistry")!
}

enum ObjectCodingError : Error {
case registryMissing
case idInUse
case typeMismatch
case wrongIdState
case graphCycle
}

class ObjectRegistry {
        typealias Id = UInt64
        public enum Presence {
        case absent
        case busy
        case present(AnyObject)
        }
        var idToPresence: [Id : Presence] = [:]
        var oidToId: [ObjectIdentifier : Id] = [:]
        var nextId: Id = 0
        public init() {
        }
        public func insert(_ o: AnyObject) throws -> Id {
                let oid = ObjectIdentifier(o)
                guard oidToId[oid] == nil else {
                        throw ObjectCodingError.idInUse
                }
                let id = nextId
                defer {
                        nextId = nextId + 1
                }
                oidToId[oid] = id
                idToPresence[id] = .present(o)
                return id
        }
        public func beginInsertion(at id: Id) throws {
                guard case .absent = idToPresence[id, default: .absent] else {
                        throw ObjectCodingError.idInUse
                }
                idToPresence[id] = .busy
        }
        public func finishInsertion(of o: AnyObject, at id: Id) throws {
                switch idToPresence[id, default: .absent] {
                case .busy:
                        idToPresence[id] = .present(o)
                case .absent:
                        throw ObjectCodingError.wrongIdState
                case .present(_):
                        throw ObjectCodingError.idInUse
                }
        }
        public func lookup(_ id: Id) -> Presence {
                return idToPresence[id, default: .absent]
        }
        public func lookup(_ o: AnyObject) -> Id? {
                return oidToId[ObjectIdentifier(o)]
        }
}

struct ObjectCodingContainer<T> : Codable where T : CodableWithIdentity {
        var instance: T
        enum CodingKeys : CodingKey {
        case id
        case object
        }
        init(containing instance: T) {
                self.instance = instance
        }
        func encode(to encoder: Encoder) throws {
                var container = encoder.container(keyedBy: CodingKeys.self)
                guard let registry =
                    encoder.userInfo[.objectRegistry] as? ObjectRegistry
                    else {
                        throw ObjectCodingError.registryMissing
                }
                if let id = registry.lookup(instance) {
                        try container.encode(id, forKey: CodingKeys.id)
                        return
                }
                let id = try registry.insert(instance)
                try container.encode(id, forKey: CodingKeys.id)
                try container.encode(instance, forKey: CodingKeys.object)
        }
        init(from decoder: Decoder) throws {
                let container = try decoder.container(keyedBy: CodingKeys.self)
                guard let registry =
                    decoder.userInfo[.objectRegistry] as? ObjectRegistry
                    else {
                        throw ObjectCodingError.registryMissing
                }
                let id = try container.decode(UInt64.self,
                    forKey: CodingKeys.id)

                switch registry.lookup(id) {
                case .present(let found):
                        guard let object = found as? T else {
                                throw ObjectCodingError.typeMismatch
                        }
                        self.instance = object
                        return
                case .busy:
                        throw ObjectCodingError.graphCycle
                case .absent:
                        try registry.beginInsertion(at: id)
                        let object = try container.decode(T.self,
                                forKey: CodingKeys.object)
                        try registry.finishInsertion(of: object, at: id)
                        self.instance = object
                }
        }
}

public protocol CodableWithIdentity : AnyObject {
        associatedtype Keys : CodingKey
        init(from: inout KeyedDecodingContainer<Keys>) throws
        func encode(to: inout KeyedEncodingContainer<Keys>) throws
}

extension CodableWithIdentity {
        init(from outer: inout UnkeyedDecodingContainer) throws {
            var inner = try outer.nestedContainer(keyedBy: Self.Keys.self)
            try self.init(from: &inner)
        }
        func encode(to outer: inout UnkeyedEncodingContainer) throws {
            var inner = outer.nestedContainer(keyedBy: Self.Keys.self)
            try self.encode(to: &inner)
        }
}

extension KeyedDecodingContainer {
        func decode<T : CodableWithIdentity>(_ type: T.Type,
            forKey key: Key) throws -> T {
                    var container =
                        try nestedContainer(keyedBy: T.Keys.self, forKey: key)
                    return try T.self.init(from: &container)
        }
}

extension KeyedEncodingContainer {
        mutating func encode<T : CodableWithIdentity>(_ o: T, forKey key: Key)
            throws {
                    var container =
                        nestedContainer(keyedBy: T.Keys.self, forKey: key)
                    return try o.encode(to: &container)
        }
}

extension UnkeyedEncodingContainer {
        mutating func encode<T : CodableWithIdentity>(_ o: T)
            throws {
                    var container =
                        nestedContainer(keyedBy: T.Keys.self)
                    return try o.encode(to: &container)
        }
}

extension Encoder {
        func encode<T>(_ o: T) throws -> T where T : CodableWithIdentity {
                let container = ObjectCodingContainer<T>(containing: o)
                return container.instance
        }
}

extension Decoder {
        func decode<T>(_ type: T.Type) throws -> T
            where T : CodableWithIdentity, T : Codable {
                let container = try ObjectCodingContainer<T>(from: self)
                return container.instance
        }
}

```

> On the decoding side, Swift's `init` design prevents the reconstruction of arbitrary graphs. The nearest you can get is to severely restrict the structure of the graph, or to fix up the decoded graph in a separate, manual pass — which may require exposing private members as public, making the whole process somewhat subject to abuse.

I see what you mean about the `init` design.

I have been thinking about performing "just-in-time" resolution  
of references in an arbitrary object graph, at some small cost in  
complexity and performance. Suppose that if instances of class `C` can  
be involved in a cycle, then we replace each Codable reference to an  
instance of `C` with a wrapped enum property that contains a reference  
either to an actual `C` instance or the information to fetch a reference  
from an `ObjectRegistry`:

```swift
@propertyWrapper enum Reference<C> {
case proxy(UInt64, ObjectRegistry)
case actual(C)
        var wrappedValue: C {
                mutating get {
                        switch self {
                        case .proxy(let id, let registry):
                                switch registry.lookup(id) {
                                case .present(let object):
                                        let instance = object as! C
                                        self = .actual(instance)
                                        return instance
                                default:
                                        fatalError()
                                }
                        case .actual(let object):
                                return object
                        }
                }
                set {
                        self = .actual(newValue)
                }
        }
}

```

Dave

---

_[View the full topic](https://forums.swift.org/t/codable-items-with-identity/60726)._
