Skip to content

[BUG] Instant fields serialize as numeric epoch timestamps instead of ISO-8601 strings in azure-cosmos #49715

Description

@pure-zero

Describe the bug

When a POJO stored/read via CosmosContainer/CosmosAsyncContainer has a java.time.Instant field, the field is serialized to Cosmos DB as a numeric epoch
value (seconds since epoch, with fractional nanoseconds) instead of an ISO-8601 date-time string.

Root cause: Utils.createAndInitializeObjectMapper() (com.azure.cosmos.implementation.Utils) registers Jackson's JavaTimeModule but never disables
SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, which defaults to true on a plain ObjectMapper. JavaTimeModule's InstantSerializer checks that flag
before choosing between epoch-numeric and ISO-8601 string output — since it's left enabled, Instant fields always serialize as a number. This ObjectMapper
is the one backing Utils.getDocumentObjectMapper(...), which CosmosItemSerializer.DEFAULT_SERIALIZER / DefaultCosmosItemSerializer use for all item
bodies, so it affects every item write/read through both the sync CosmosContainer and CosmosAsyncContainer (the sync client is a thin blocking wrapper
around the async one and shares the same serializer).

Other java.time types handled by JavaTimeModule (e.g. LocalDateTime) are affected the same way for the same reason.

Exception or Stack Trace

None — no exception is thrown. The item is written/read successfully, but the Instant field's JSON representation is wrong (a number instead of a string),
which silently breaks any downstream consumer (other SDKs, the Data Explorer UI, queries filtering on the field as a string, etc.) that expects an ISO-8601
date-time string.

To Reproduce

  1. Define a POJO with an Instant field.
  2. Write an instance to a container with createItem/upsertItem.
  3. Read the raw item JSON back (e.g. via the Azure Portal Data Explorer, or container.readItem(id, pk, JsonNode.class)).
  4. Observe the field is a number, not a quoted ISO-8601 string.

Code Snippet

public class Event {
    public String id;
    public Instant createdAt;
}

CosmosClient client = new CosmosClientBuilder()
    .endpoint(endpoint)
    .key(key)
    .buildClient();

CosmosContainer container = client.getDatabase("db").getContainer("events");

Event event = new Event();
event.id = "1";
event.createdAt = Instant.parse("2023-07-22T09:46:40.123456789Z");

container.createItem(event);

// Read back the raw JSON to inspect the wire format
ObjectNode raw = container.readItem("1", new PartitionKey("1"), ObjectNode.class).getItem();
System.out.println(raw.toPrettyString());

Expected behavior

{
  "id": "1",
  "createdAt": "2023-07-22T09:46:40.123456789Z"
}

Actual behavior

{
  "id": "1",
  "createdAt": 1690019200.123456789
}

Screenshots

N/A

Setup (please complete the following information):
- OS: N/A (reproducible on any OS)
- IDE: N/A
- Library/Libraries: com.azure:azure-cosmos:4.81.0
- Java version: 8+ (reproduced on Java 8 baseline; affects all supported Java versions since it's a Jackson configuration issue, not JVM-specific)
- App Server/Environment: N/A (reproducible in a plain main(), not environment-specific)
- Frameworks: N/A (does not require Spring/other frameworks; also affects azure-spring-data-cosmos since it depends on the same default serializer)

Additional context

- Same file, Utils.createAndInitializeDurationObjectMapper(), shows the team is aware Instant needs special handling to avoid Jackson's numeric default — it
explicitly registers ToStringSerializer.instance for Instant/Duration on a separate mapper used for diagnostics. This strongly suggests the missing
WRITE_DATES_AS_TIMESTAMPS = false on the main document mapper is an oversight, not an intentional design choiceJavaTimeModule appears to have been added in
an earlier PR purely to make Instant deserializable, without considering the serialization-format side effect.
- Workaround: supply a custom CosmosItemSerializer via CosmosClientBuilder.customItemSerializer(...) (client-wide) or
CosmosItemRequestOptions.setCustomItemSerializer(...) (per-request) backed by an ObjectMapper with JavaTimeModule registered and
SerializationFeature.WRITE_DATES_AS_TIMESTAMPS explicitly set to false.
- Compatibility note for the fix: changing this default is a breaking wire-format change for any existing data already written with Instant fields in epoch
formatneeds a migration/compat plan, not a silent patch, since consumers may have already adapted to reading these fields as numbers.

Information Checklist
- [x] Bug Description Added
- [x] Repro Steps Added
- [x] Setup information Added

Metadata

Metadata

Assignees

No one assigned

    Labels

    ClientThis issue points to a problem in the data-plane of the library.CosmosService AttentionWorkflow: This issue is responsible by Azure service team.customer-reportedIssues that are reported by GitHub users external to the Azure organization.needs-team-attentionWorkflow: This issue needs attention from Azure service team or SDK teamquestionThe issue doesn't require a change to the product in order to be resolved. Most issues start as that

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions