iOS error 'Invalid type in JSON write (FIRTimestamp)'
Asked Answered
G

4

6

I am trying to map my data to Model. Where I am using Firestore snapshot listener, to get data. here, I am getting data and mapping to "User" model that;

            do{
                let user = try User(dictionary: tempUserDic)
                print("\(user.firstName)")
            }
            catch{
                print("error occurred")
            }

Here is my Model:

struct User {
    let firstName: String
//    var lon: Double = 0.0
//    var refresh:Int = 0
//    var createdOn: Timestamp = Timestamp()
}

//Testing Codable
extension User: Codable {
    init(dictionary: [String: Any]) throws {
        self = try JSONDecoder().decode(User.self, from: JSONSerialization.data(withJSONObject: dictionary))
    }
    private enum CodingKeys: String, CodingKey {
        case firstName = "firstName"
    }
}

Correct me if I am wrong.

Crashing because I am getting "Timestamp" in data.

Data getting from listener :

User Dictionary:

[\"firstName\": Ruchira,
 \"lastInteraction\": FIRTimestamp: seconds=1576566738 nanoseconds=846000000>]"

How to map "Timestamp" to Model?

Tried "CodableFirstore" https://github.com/alickbass/CodableFirebase

Gluconeogenesis answered 18/12, 2019 at 10:38 Comment(4)
The code doesn't write anything. To map the data to a model you have to make Timestamp conform to Codable. ObjectMapper is not necessary.Irregular
@Irregular I am not using "ObjectMapper". when I am trying to use "Timestamp" in Codable, it is not allowing me.Gluconeogenesis
Use a representation of FIRTimestamp which is Codable compatible like Date.Irregular
I think this error is from JSONSerializaton not from CodableShelley
M
2

An approach is to create an extension to type Dictionary that coverts a dictionary to any other type, but automatically modifies Date and Timestamp types to writeable JSON strings.

This is the code:

extension Dictionary {

    func decodeTo<T>(_ type: T.Type) -> T? where T: Decodable {
        
        var dict = self

        // This block will change any Date and Timestamp type to Strings
        dict.filter {
            $0.value is Date || $0.value is Timestamp
        }.forEach {
            if $0.value is Date {
                let date = $0.value as? Date ?? Date()
                dict[$0.key] = date.timestampString as? Value
            } else if $0.value is Timestamp {
                let date = $0.value as? Timestamp ?? Timestamp()
                dict[$0.key] = date.dateValue().timestampString as? Value
            }
        }

        let jsonData = (try? JSONSerialization.data(withJSONObject: dict, options: [])) ?? nil
        if let jsonData {
            return (try? JSONDecoder().decode(type, from: jsonData)) ?? nil
        } else {
            return nil
        }
    }
}

The .timestampString method is also declared in an extension for type Date:

extension Date {
    
    var timestampString: String {
        Date.timestampFormatter.string(from: self)
    }
    
    static private var timestampFormatter: DateFormatter {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        dateFormatter.timeZone = TimeZone(identifier: "UTC")
        return dateFormatter
    }
}

Usage, like in the case of the question:

let user = tempUserDict.decodeTo(User.self)
Metallic answered 22/11, 2022 at 5:59 Comment(0)
C
0

I solved this by converting the FIRTimestamp fields to Double (seconds) so the JSONSerialization could parse it accordingly.

 let items: [T] = documents.compactMap { query in
   var data = query.data() // get a copy of the data to be modified.

   // If any of the fields value is a `FIRTimestamp` we replace it for a `Double`.
   if let index = (data.keys.firstIndex{ data[$0] is FIRTimestamp }) {

     // Convert the field to `Timestamp`
     let timestamp: Timestamp = data[data.keys[index]] as! Timestamp

     // Get the seconds of it and replace it on the `copy` of `data`.
     data[data.keys[index]] = timestamp.seconds

    }

    // This should not complain anymore.
    guard let data = try? JSONSerialization.data(
       withJSONObject: data, 
       options: .prettyPrinted
    ) else { return nil }

    // Make sure your decoder setups the decoding strategy to `. secondsSince1970` (see timestamp.seconds documentation).
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .secondsSince1970
    return try? decoder.decode(T.self, from: data)
 }

// Use now your beautiful `items`
return .success(items)

Carbon answered 29/9, 2022 at 7:46 Comment(0)
B
0

I found that Firestore natively handles timestamp conversion via documentReference.data(as: ); so instead of accessing the data dictionary yourself, you can pass it in such as: documentReference.data(as: User.self).

The existing solutions implement additional decoding logic which I believe should already be handled by Timestamp's conformance to Codable. And they'll run into issues with nested timestamps.

  • Via firebase docs: Use documentReference.data(as: ) to map a document reference to a Swift type.
Boutte answered 11/10, 2023 at 16:19 Comment(0)
D
0

While using the build in Firebase decoder is ideal, a change in the structure of the model class will render it broken. In my case, I catch the error and then correct the incoming record data so it can be decoded. Here is a method that expands upon @mig_loren answer but fixes all properties including recursing through arrays and child dictionaries.

private func fixFirTimestamps(_ jsonDict: [String: Any]) -> [String: Any] {
    var fixedDict = jsonDict
    
    // Find all fields where value is a `FIRTimestamp` and replace it with `Double`.
    let indices = fixedDict.keys.indices.filter { index in
        let key = fixedDict.keys[index]
        return jsonDict[key] is Timestamp
    }
    
    // replace `Timestamp` with `Double`
    for index in indices {
        let key = fixedDict.keys[index]
        guard let timestamp = fixedDict[key] as? Timestamp else { continue }
        fixedDict[key] = timestamp.seconds
    }

    // find child json and recurse
    let childJsonIndices = fixedDict.keys.indices.filter { index in
        let key = fixedDict.keys[index]
        return jsonDict[key] is [String: Any]
    }
    
    // recurse child json
    for index in childJsonIndices {
        let key = fixedDict.keys[index]
        guard let childJson = fixedDict[key] as? [String: Any] else { continue }
        fixedDict[key] = fixFirTimestamps(childJson)
    }
    
    // find arrays of child json and recurse
    let childJsonArrayIndices = fixedDict.keys.indices.filter { index in
        let key = fixedDict.keys[index]
        return jsonDict[key] is [[String: Any]]
    }
    
    // recurse child json
    for index in childJsonArrayIndices {
        let key = fixedDict.keys[index]
        guard let childJsonArray = fixedDict[key] as? [[String: Any]] else { continue }
        let updatedArray = childJsonArray.map { childJson in
            return fixFirTimestamps(childJson)
        }
        fixedDict[key] = updatedArray
    }
    
    return fixedDict
}

// call with 
let fixedDict = fixFirTimestamps(query.data())
Dominus answered 22/9 at 17:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.