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

@DocumentID not populated with custom codable implementation #7242

Closed
everlof opened this issue Jan 3, 2021 · 1 comment
Closed

@DocumentID not populated with custom codable implementation #7242

everlof opened this issue Jan 3, 2021 · 1 comment
Assignees

Comments

@everlof
Copy link

everlof commented Jan 3, 2021

[REQUIRED] Step 1: Describe your environment

  • Xcode version: 12.2
  • Firebase SDK version: 7.3.0
  • Installation method: Swift Package Manager
  • Firebase Component: Firestore

[REQUIRED] Step 2: Describe the problem

I've taken a look into the codable API, which looks really nice! I've watched https://www.youtube.com/watch?v=3-yQeAf3bLE which links to "Encoding and Decoding Custom Types → https://goo.gle/2T1Quic".

However, I haven't found enough resources about how to use Codable with:

  • Firestore
  • Custom types (implement init(from:Decoder + encode(to:Encoder))
  • @DocumentID property wrapper

If that case is supported, I think it would be great to have some examples of how to use it, if it's not supported, perhaps that should be mentioned somewhere.

Steps to reproduce:

Create a struct with @DocumentID, CodingKeys and implement public init(from decoder: Decoder) throws as well as public func encode(to encoder: Encoder) throws.

How is the property decorated with @DocumentID supposed to be encoded/decoded? The only way I've been able to figure it out is by getting it from the userInfo, which obviously is wrong.

Relevant Code

struct TestObject: Codable {
    public enum CodingKeys: String, CodingKey {
        case ref = "r"
    }

    @DocumentID
    public var ref: DocumentReference?

    public init() {
        ref = nil
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        ref = try container.decodeIfPresent(DocumentReference.self, forKey: .ref)
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encodeIfPresent(ref, forKey: .ref)
    }
}

class SomeTests: XCTestCase {

    lazy var firestore: Firestore = {
        let path = Bundle(for: SomeTests.self).url(forResource: "GoogleService-Info", withExtension: "plist")!.path
        if FirebaseApp.app() == nil {
            FirebaseApp.configure(options: FirebaseOptions(contentsOfFile: path)!)
        }

        let firestore = Firestore.firestore()
        let settings = firestore.settings
        settings.host = "localhost:8080"
        settings.isPersistenceEnabled = false
        settings.isSSLEnabled = false
        firestore.settings = settings
        return firestore
    }()

    func testDocumentId() {
        let e = expectation(description: "async")
        let t = TestObject()
        _ = try! firestore.collection("test").addDocument(from: t, encoder: Firestore.Encoder()) { error in
            if let error = error {
                XCTFail(error.localizedDescription)
            } else {
                self.firestore.collection("test").addSnapshotListener { (snapshot, error) in
                    if let snapshot = snapshot,
                       let document = snapshot.documents.first
                    {
                        let testObject = try! document.data(as: TestObject.self)!
                        XCTAssertNotNil(testObject.ref) // <- this is not decoded
                        e.fulfill()
                    }
                }
            }
        }

        wait(for: [e], timeout: 1.0)
    }

}
@everlof everlof changed the title @DocumentID no populated with custom coddle impl @DocumentID not populated with custom codable implementation Jan 3, 2021
@thebrianchen thebrianchen self-assigned this Jan 4, 2021
@wilhuff wilhuff assigned wilhuff and unassigned thebrianchen Jan 5, 2021
wilhuff added a commit that referenced this issue Jan 5, 2021
wilhuff added a commit that referenced this issue Jan 5, 2021
@wilhuff
Copy link
Contributor

wilhuff commented Jan 5, 2021

I wasn't sure this actually worked, but #7247 shows that this is possible and works.

The issue here is an unfortunate sharp edge in the intersection between property wrappers and Codable. In this code, this field does not have the type you think it does:

@DocumentID
public var ref: DocumentReference?

While this looks like a field with the type DocumentReference?, it's actually a field with the type DocumentID<DocumentReference>. Swift property wrappers work by transparently inserting calls to wrappedValue and init(wrappedValue:) as required. This transparency is even reflected in the result of type(of: self.ref).

The problem here is that DocumentReference by itself actually is a Codable type (when used in conjunction with the FirestoreEncoder), but in that case the reference would actually have to be in the data you're decoding. When DocumentReference is wrapped by DocumentID it gets this magical behavior of auto-population. That is, during the Codable traversal of the structure, DocumentID is not hidden, and that's where all the auto-population magic actually happens.

Unfortunately, we can't really compensate for this. At the point where you call

ref = try container.decodeIfPresent(DocumentReference.self, forKey: .ref)

there's no reflective means by which we could determine that you really meant to get the document ID. This is really requesting that the document have a field in it named r whose value is a document reference you've actually stored.

The solution is to explicitly ask for the DocumentID wrapping of DocumentReference and then manually unwrap yourself. It looks like this:

ref = try container.decode(DocumentID<DocumentReference>.self, forKey: .ref)
  .wrappedValue

Also note that your encode(to:) method should not encode any DocumentID annotated fields. This will actually write the value out into the stored document which you don't actually want. This will work but then on the next decode, Firestore will throw an error indicating the ambiguity between the field you've stored and the annotated field.

See the linked PR for a working example.

@wilhuff wilhuff closed this as completed Jan 5, 2021
@firebase firebase locked and limited conversation to collaborators Feb 5, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

4 participants