/* * Copyright 2025 Google. This software is provided as-is, without warranty or representation for any use or purpose. * Your use of it is subject to your agreement with Google. */ package com.example.util; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.google.cloud.Timestamp; import java.io.IOException; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Custom Jackson Deserializer for com.google.cloud.Timestamp. * Handles deserialization from embedded objects (direct Timestamp instances), * ISO 8601 strings, and JSON objects with "seconds" and "nanos" fields. */ public class FirestoreTimestampDeserializer extends JsonDeserializer { private static final Logger logger = LoggerFactory.getLogger(FirestoreTimestampDeserializer.class); @Override public Timestamp deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { JsonToken token = p.getCurrentToken(); if (token == JsonToken.VALUE_EMBEDDED_OBJECT) { // This is the ideal case when ObjectMapper.convertValue gets a direct Timestamp object Object embedded = p.getEmbeddedObject(); if (embedded instanceof Timestamp) { logger.debug("FirestoreTimestampDeserializer: Deserializing from embedded Timestamp object: {}", embedded); return (Timestamp) embedded; } } else if (token == JsonToken.VALUE_STRING) { // Handles cases where the timestamp is represented as an ISO 8601 string String timestampString = p.getText(); try { logger.debug("FirestoreTimestampDeserializer: Deserializing from String: {}", timestampString); return Timestamp.parseTimestamp(timestampString); } catch (IllegalArgumentException e) { logger.error("FirestoreTimestampDeserializer: Failed to parse timestamp string: '{}'", timestampString, e); throw new IOException("Failed to parse timestamp string: " + timestampString, e); } } else if (token == JsonToken.START_OBJECT) { // This is crucial for handling the "Cannot deserialize ... from Object value (token JsonToken.START_OBJECT)" error. // It assumes the object represents { "seconds": X, "nanos": Y } logger.debug("FirestoreTimestampDeserializer: Deserializing from JSON object."); // Suppress the unchecked warning here, as we expect a Map @SuppressWarnings("unchecked") Map map = p.readValueAs(Map.class); if (map != null && map.containsKey("seconds") && map.containsKey("nanos")) { Number secondsNum = map.get("seconds"); Number nanosNum = map.get("nanos"); if (secondsNum != null && nanosNum != null) { Long seconds = secondsNum.longValue(); Integer nanos = nanosNum.intValue(); return Timestamp.ofTimeSecondsAndNanos(seconds, nanos); } } logger.error("FirestoreTimestampDeserializer: JSON object missing 'seconds' or 'nanos' fields, or fields are not numbers."); } // If none of the above formats match, log an error and delegate to default handling logger.error("FirestoreTimestampDeserializer: Unexpected token type for Timestamp deserialization. Expected Embedded Object, String, or START_OBJECT. Got: {}", token); // This will likely re-throw an error indicating inability to deserialize. return (Timestamp) ctxt.handleUnexpectedToken(Timestamp.class, p); } }