alickbass / CodableFirebase

Use Codable with Firebase
MIT License
691 stars 91 forks source link

Decoding Timestamp and Geopoint from Firestore #50

Closed flashadvanced closed 6 years ago

flashadvanced commented 6 years ago

Hey lads, In my Firestore data I keep timestamp and geopoint fields. In my Swift model I keep them like this:

struct MyFirestoreModel: Codable {
    let dateAdded: Timestamp
    let location: GeoPoint
}

When I use FirebaseDecoder though I get these errors:

Timestamp

typeMismatch(Swift.Double, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "dateAdded", intValue: nil)], debugDescription: "Expected to decode Double but found FIRTimestamp instead.", underlyingError: nil))

Geopoint

typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "location", intValue: nil)], debugDescription: "Not a dictionary", underlyingError: nil))

Any ideas what I am doing wrong? Cheers

flashadvanced commented 6 years ago

@alickbass any thoughts on this? How are we supposed to decode Timestamp and GeoPoint with FirestoreDecoder? Thanks

serjooo commented 6 years ago

@flashadvanced Did you have Geopoint and Timestamp extend and conform to the following protocols:

GeoPointType TimestampType

The README file explicitly mentions this:

flashadvanced commented 6 years ago

Hey @serjooo, sure I conform to them both in my project:

extension GeoPoint: GeoPointType {}
extension Timestamp: TimestampType {}
serjooo commented 6 years ago

Have you maybe changed your date decoding strategy? Maybe that's why it tells you Expected to decode Double. As for the Geopoint it does inform you that it isn't finding a dictionary to decode. As Geopoint is an object with the two properties latitude and longitude make sure that from Firestore that's what is exactly being sent

flashadvanced commented 6 years ago

I haven't done anything special, I just make the expected attributes from Firestore db to be of Timestamp and GeoPoint types. I also logged the incoming data to make sure what's going on:

- key : "location"
- value : <FIRGeoPoint: (42.125881, 24.791203)>
- key : "dateAdded"
- value : FIRTimestamp: seconds=1536087940 nanoseconds=611000000>

Which means everything is as it should be, right?

serjooo commented 6 years ago

Yup the dictionary looks good, should be working... I used the above method for the FIRTimestamp and it worked on my end. Are you able to encode your own objects and persist them to Firestore?

flashadvanced commented 6 years ago

Yeah, values are set to Firestore cloud as expected. For the location attribute I just set GeoPoint(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude) and for the dateAdded attribute I use FieldValue.serverTimestamp(). This really makes no sense :)

serjooo commented 6 years ago

Can you show me the code where you are encoding the MyFirestoreModel and saving it to Firestore

flashadvanced commented 6 years ago

Sure. As explained I set the location like this: modelToEncode.location = GeoPoint(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude) For the timestamp field I do it a bit differently. I don't keep an attribute in the struct I am encoding. First I encode the model with FirestoreEncoder and then I get the result dictionary and mutate it to add the timestamp field like this (I use a helper function):

func withCreationTimestamp() -> Dictionary {
    var modified = self
    modified["dateAdded"] = FieldValue.serverTimestamp()
    return modified
}

Finally I set the encoded model to the Firestore document reference.

serjooo commented 6 years ago

That's exactly your problem, you are saving to Firestore the dateAdded as a double value which is the FieldValue.serverTimestamp(), not sure what you are doing with the Geopoint unless I look at it. However, you shouldn't be writing those dictionaries manually, eventhough you can if you please, there is an easier way of persisting your model to Firestore and that is using the FirestoreEncoder.

Let me provide you with the code:

let encoder = FirestoreEncoder()
let dictionary = encoder.encode(myFirestoreModel)

Now that you used the FirestoreEncoder to encode your model object into a dictionary to save to Firestore, it will be saved to Firestore with the correct types. So in return when you use the FirestoreDecoder to decode MyFirestoreModel it should work without a problem!

flashadvanced commented 6 years ago

I decided to go with FieldValue.serverTimestamp() as this was the recommended way of saving timestamp to Firestore. Anyways I removed the mutated encoded dictionary part and just set the attribute as default value like this: init(dateAdded: Timestamp = Timestamp(date: Date(), ...) which should be valid right? Again it sets the date attribute to Firestore as expected but it cannot be handled properly by the Decoder (prints out the same error).

serjooo commented 6 years ago

You will need to delete/modify all the objects that have been saved as a Double for dateAdded so that when you decode with FirestoreDecoder it decodes properly. As for the initializer it looks good. As for the decoder still failing, the only thing I can say is make sure you are persisting the dictionary correctly. If all else fails, I'll try making a project that uses FIRTimestamp and FIRGeopoint with CodableFirebase so that you could try out tomorrow.

flashadvanced commented 6 years ago

Thanks for everything, highly appreciated.

serjooo commented 6 years ago

@flashadvanced were you able to figure it out and make it work or should I share a demo project with you?

flashadvanced commented 6 years ago

@serjooo yes please, if you can share the demo project that would be great.

serjooo commented 6 years ago

@flashadvanced check this project of mine. It is using FIRTimestamp and I am able to encode and decode easily to get the Timestamp value

flashadvanced commented 6 years ago

@serjooo, thanks a lot for sharing this. It really helped me to locate my silly mistake. I finally realized what I was doing wrong - I was using FirebaseDecoder instead of FirestoreDecoder. Now everything works as expected - both Timestamp and GeoPoint attributes.

luistejadaa commented 6 years ago

@serjooo, thanks a lot for sharing this. It really helped me to locate my silly mistake. I finally realized what I was doing wrong - I was using FirebaseDecoder instead of FirestoreDecoder. Now everything works as expected - both Timestamp and GeoPoint attributes.

I also had the same problem, I did not realize I was using FirebaseDecode, thank you very much.

serjooo commented 6 years ago

@flashadvanced @luistejadaa very glad that the project shared helped you spot what was going wrong! Great job!

flashadvanced commented 6 years ago

Seeing other people making the same mistake I think it is a good idea having FirebaseDecoder renamed to let say RealtimeDecoder. Because Firestore is still a Firebase product and FirebaseDecoder and FirestoreDecoder are both part of the same pod it is really easy to mess things up and then debug for long time not knowing what is going on. What do you guys think? @alickbass

lozflan commented 5 years ago

serjoo. I've got problems working with FieldValue.serverTimestamp(). i want to have a timestamp in my model object and use FieldValue.serverTimestamp() as a placeholder. then encode the object and save to firestore. have firestore populate the timestamp field. later fetch the object and decode back to a timestamp on the clients. is that essentially what you've achieved in your test project above. I've downloaded it but can't see where such code is?

serjooo commented 5 years ago

@lozflan yes if I remember correctly in that project I'm using the Timestamp and not the Date object.

lozflan commented 5 years ago

I can't work out how to do this. If i have an model IDRequest as follows Struct IDRequest: Codable { var uid: String? var lastUpdated: Timestamp? } and then i want to create an instance like so let idRequest = IDRequest(uid: "abc", lastUpdated: FieldValue.serverTimestamp()) so that firestore later creates the timestamp, not the client. and then encoding with
var docData = try! FirestoreEncoder().encode(idRequest)

How do i get the compiler to stop complaining "Cannot convert value of type 'FieldValue' to expected argument type 'Timestamp?'

I don't want to have to munge the docData dictionary manually after creating it to add lastUpdated to it because doesn't that defeat the purpose of this whole "codable" exercise

serjooo commented 5 years ago

@lozflan If you want to create a Timestamp instance you need to use the appropriate constructors of the Timestamp object. I recommend looking at the Timestamp or FIRTimestamp object and documentation. However, to make it easier you would need to do something like the following:

IDRequest(uid: "abc", lastUpdated: Timestamp.init(date: Date()))

lozflan commented 5 years ago

@serjooo thx I've got it working with Timestamp but Ive got a strange problem with the extension line ie extension Timestamp: TimestampType {}. if i place this extension in my app everything works fine and I can encode and decode a test MyFirestoreModel object below. My app has a custom framework and if i move the Timestamp extension there, it encodes fine but crashes on decoding with following error message ...

Thread 1: Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.typeMismatch(Foundation.Date, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "dateAdded", intValue: nil)], debugDescription: "Expected to decode Date but found FIRTimestamp instead.", underlyingError: nil))

There is no other change if i comment out the line extension Timestamp: TimestampType {} in my custom framework and uncomment the same line in my app, the myFirestoreModel encodes and decodes fine. If i do the opposite, the app crashes with the above error message.

struct MyFirestoreModel: Codable { let dateAdded: Timestamp }

My code (ignoring error checking)

` let myMod = MyFirestoreModel(dateAdded: Timestamp(date: Date())) let myData = try! FirestoreEncoder().encode(myMod)

    db.collection("idRequests").document("mfm").setData(myData) { (err) in
        self.db.collection("idRequests").document("mfm").getDocument(completion: { (snapshot, err) in
            let dic = snapshot?.data()
            print("dic:\(dic!)")
            let myModelReconstituted = try! FirestoreDecoder().decode(MyFirestoreModel.self, from: dic!) // CRASHES here if extension Timestamp: TimestampType {}  declared in custom framework

        })
    }`
serjooo commented 5 years ago

@lozflan I'm not sure if this might be it, but did you try making the extension public so that your project itself can also use that extension?