Azure / Azure/azure-sdk-for-java

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

Open
#49,715 2 comments 1 reaction 3 assignees Claimed by @TheovanKraay View on GitHub
bug Client Cosmos customer-reported needs-team-attention Service Attention
Dominant language
Java
Stars
2.6k
Forks
2.2k
Avg merge
2d 8h
Merged PRs (30d)
178

Description

**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***

```java
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 choice — JavaTimeModule 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
format — needs 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

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.