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
- Define a POJO with an
Instant field.
- Write an instance to a container with
createItem/upsertItem.
- Read the raw item JSON back (e.g. via the Azure Portal Data Explorer, or
container.readItem(id, pk, JsonNode.class)).
- 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 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
Describe the bug
When a POJO stored/read via
CosmosContainer/CosmosAsyncContainerhas ajava.time.Instantfield, the field is serialized to Cosmos DB as a numeric epochvalue (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'sJavaTimeModulebut never disablesSerializationFeature.WRITE_DATES_AS_TIMESTAMPS, which defaults totrueon a plainObjectMapper.JavaTimeModule'sInstantSerializerchecks that flagbefore choosing between epoch-numeric and ISO-8601 string output — since it's left enabled,
Instantfields always serialize as a number. ThisObjectMapperis the one backing
Utils.getDocumentObjectMapper(...), whichCosmosItemSerializer.DEFAULT_SERIALIZER/DefaultCosmosItemSerializeruse for all itembodies, so it affects every item write/read through both the sync
CosmosContainerandCosmosAsyncContainer(the sync client is a thin blocking wrapperaround the async one and shares the same serializer).
Other
java.timetypes handled byJavaTimeModule(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
Instantfield'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
Instantfield.createItem/upsertItem.container.readItem(id, pk, JsonNode.class)).Code Snippet