supabase-swift icon indicating copy to clipboard operation
supabase-swift copied to clipboard

Supabase Swift does not encode / decode fractional seconds on Date types

Open chrisrohrer opened this issue 8 months ago • 2 comments

Bug report

Supabase Swift does not encode / decode fractional seconds on Date types

Which makes it very problematic for syncing use-cases.

Reason might be the API uses the standard Swift JSONEncoder/Decoder .iso8601 which does not support that. Has to be a custom Encoder/Deocder.

chrisrohrer avatar Apr 07 '25 19:04 chrisrohrer

Hi @chrisrohrer do you have a JSON example which breaks the current decoder instance?

grdsdev avatar Apr 07 '25 21:04 grdsdev

Hi @grdsdev sure, here is a test code setup. In the retrieved date fractional seconds are always .000Z

I suppose they are already skipped in the encode at .insert(item)

SQL

CREATE TABLE public."Test" (
    id bigint primary key generated always as identity,
    date timestamp with time zone
);

SWIFT

struct TestItem: Identifiable, Codable {
    var id: Int?
    var date: Date
}


func dateCodingTest() {
    
    Task {
        
        // formatter to display fractional seconds
        let formatter = ISO8601DateFormatter()
        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]

        do {
            
            let item = TestItem(date: .now)
            print(formatter.string(from: item.date), "- set date with fractional secs")
            
            let response = try await supabase
                .from("Test")
                .insert(item)
                .execute()

            print(response.status)

            let serverRecords: [TestItem] = try await supabase
                .from("Test")
                .select()
                .order("date", ascending: true)
                .execute()
                .value
            
            let last = serverRecords.last!
            
            print(formatter.string(from: last.date), "- retrieved date without fractional secs")
            
        } catch {
            print(#function, error)
        }
    }
}

chrisrohrer avatar Apr 08 '25 15:04 chrisrohrer

I'm getting the same error on a realtime subscription;

channel.onPostgresChange(AnyAction.self,
                         table: "chat_members",
                         filter: "user_id=eq.\(me.id)") { action in
      do {
        switch action {
          // ----- INSERT
        case .insert(let insert):
          print("Was added to chat: \(insert.record)")
          let chat = try insert.decodeRecord(as: ChatMember.self, decoder: .supabaseCompatibleDecoder)
          self.fetchAndAddChat(id: chat.chat_id)
          break
          // ...

Error:

- Failed to update chat relationship! dataCorrupted(Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "joined_at", intValue: nil)], debugDescription: "Expected date string to be ISO8601-formatted with fractional seconds", underlyingError: nil))
My `.supabaseCompatibleDecoder`:
struct ISODateFormatter {
  let isoFormatterWithFractional = ISO8601DateFormatter()
  let isoFormatterWithoutFractional = ISO8601DateFormatter()
  
  init() {
    isoFormatterWithFractional.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
    isoFormatterWithoutFractional.formatOptions = [.withInternetDateTime]
  }
  
  func date(from string: String) -> Date? {
    if let date = isoFormatterWithFractional.date(from: string) {
      return date
    }
    if let date = isoFormatterWithoutFractional.date(from: string) {
      return date
    }
    return nil
  }
}

extension JSONDecoder {
  static var supabaseCompatibleDecoder: JSONDecoder = {
    let formatter = ISODateFormatter()
    
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .custom { decoder in
      var container = try decoder.singleValueContainer()
      let dateString = try container.decode(String.self)
      guard let date = formatter.date(from: dateString) else {
        throw DecodingError.dataCorruptedError(
          in: container,
          debugDescription: "Expected date string to be ISO8601-formatted with fractional seconds"
        )
      }
      return date
    }
    return decoder
  }()
}

..not sure if I am using the realtime API correctly though, are we always expected to use the .decodeRecord(as: ..., decoder: ...) API?

mrousavy avatar Apr 12 '25 14:04 mrousavy

Nevermind, sorry - I just rolled my own JSONDecoder which didn't work - I now found PostgrestClient.Configuration.jsonDecoder which does the trick.

Maybe this should be added to the realtime docs, or an API should be added to the postgres listener that can just decode with the internal encoder/decoder so people don't write their own? I could look into adding this to the client library.

mrousavy avatar Apr 12 '25 14:04 mrousavy