-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Description
Describe the bug
I have a question and a potential bug related to the serialization and deserialization of data saved in JSON format in Cosmos DB using the Cosmos SDK. It relates to the customization of serialization/deserialization and the use of customItemSerializer.
Context
We have special requirements for how data should be serialized and deserialized to/from Cosmos DB. For example:
- Serialize datetime types as ISO strings (disabling
SerializationFeature.WRITE_DATES_AS_TIMESTAMPS). - Deserialize datetime values without adjusting to the context timezone (disabling
DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE). - Register
subTypesto be used with@JsonTypeInfo, etc.
Historically (around 01/2022), the Cosmos SDK did not expose any public API for customizing serialization or the ObjectMapper. We found a workaround by reconfiguring the internal ObjectMapper (Utils.getSimpleObjectMapper()). We were aware this wasn’t the cleanest solution, but it worked.
With Cosmos SDK 4.66.1 (#44035), this hack became more complicated, as reconfiguration of another internal ObjectMapper, used by DefaultCosmosItemSerializer, and accessible only via the reflection API, is now also required.
We would like to finally get rid of this nasty hack and switch to using the customItemSerializer introduced in 4.59.0 (#38997).
Unfortunately, we have encountered issues with the usage of customItemSerializer that make it nearly unusable. I’d like to ask whether we are using/configuring it incorrectly, or if it is a bug in the Cosmos SDK.
Issue 1 – Deserialization errors in SELECT containing operations like SUM, ORDER BY, GROUP BY
The first issue occurs during deserialization when a SELECT used in CosmosAsyncContainer#queryItems includes operations like VALUE, aggregations (COUNT, GROUP BY, ...), ORDER BY, and similar. When customItemSerializer is configured, such queries fail during deserialization.
Code snippet with configuration, query, and exception
// Configuration
new CosmosClientBuilder()
.customItemSerializer(new CosmosItemSerializer() {
// Prepare/inject configuredObjectMapper in constructor
public <T> Map<String, Object> serialize(T item) {
return configuredObjectMapper.convertValue(item, Map.class);
}
public <T> T deserialize(Map<String, Object> jsonNodeMap, Class<T> classType) {
return configuredObjectMapper.convertValue(jsonNodeMap, classType);
}
})
.buildAsyncClient();
// Select with ORDER BY
String query = "SELECT * FROM c ORDER BY c.id";
cosmosAsyncContainer.queryItems(new SqlQuerySpec(query), clazz);
// Exception
java.lang.NullPointerException: Cannot invoke "Object.getClass()" because "object" is null
at com.azure.cosmos.implementation.query.orderbyquery.OrderByRowResult.getPayload(OrderByRowResult.java:43)
at com.azure.cosmos.implementation.query.OrderByDocumentQueryExecutionContext$ItemToPageTransformer.lambda$apply$5(OrderByDocumentQueryExecutionContext.java:695)
As I have discovered, this is caused by the more complex way, how the deserialization internally works in Cosmos SDK:
- In the first step, the result from the database is deserialized into an internal object like
Document(JsonSerializable), which has a structure that varies depending on the query. Usually, it contains not only the fields requested in the SELECT but also metadata used by the Cosmos SDK. - In the second step,
ValueUnwrapCosmosItemSerializerconverts the data retrieved from theDocumentto the requested data type.- It also unwraps the VALUE if necessary.
- Finally, it uses ObjectMapper to convert the value into the requested data type.
The problem is that customItemSerializer is called twice in these cases:
- The first time is during an attempt to deserialize to
Document(JsonSerializable), which does not work with the ObjectMapper. Cosmos SDK uses a special method,JsonSerializable.instantiateFromObjectNodeAndType, for this. - The second time is for the actual value that should be returned as the result to the application. This may also not work because the actual value might still be wrapped in a
VALUEfield. Cosmos SDK uses special behavior inValueUnwrapCosmosItemSerializerto handle this.
I believe the custom serializer should be called only once, for the final value that is supposed to be returned as the query result to the application. It should not be called for internal Cosmos structures.
Issue 2 - SqlParameter serialization
The second issue relates to querying data from the database using CosmosAsyncContainer#queryItems and the serialization of parameters in SqlParameter. SqlParameter currently uses the internal Utils.simpleObjectMapper for serialization.
For example, when a datetime field is serialized (saved) as an ISO string in the database and filtering by this field is required, it is also necessary to serialize the SqlParameter as an ISO string. Otherwise, no results will be found, as LocalDate is by default serialized as an array.
Code snippet with a query example
String query = "SELECT * FROM c WHERE c.creationDate = @creationDate";
List<SqlParameter> params = List.of(new SqlParameter("@creationDate", LocalDate.of(2025, 5, 27)));
cosmosAsyncContainer.queryItems(new SqlQuerySpec(query, params), clazz);
What is the recommended way to handle this? Should the SDK apply customItemSerializer to SqlParameter serialization as well, or is it expected that SDK users manually serialize values (e.g., to strings) before passing them to SqlParameter? The latter approach is less user-friendly, as it requires manual step with serialization.
Exception or Stack Trace
Exceptions thrown when executing SELECT with operations like SUM, ORDER BY, etc., and with customItemSerializer configured, differ depending on the specific operation used in the query. One example of an exception for ORDER BY is shown in the section above (Issue 1 description). Exceptions for other queries are available in the reproducer project. Snippets with a few exception lines are included in the test comments, while more detailed output is available in the test report. The full stack trace can also be examined by running the tests in the reproducer project.
To Reproduce
Register a customItemSerializer (CosmosItemSerializer) to the CosmosAsyncClient, using the ObjectMapper#convertValue method for conversion. Then, execute a problematic query, for example, SELECT * FROM c ORDER BY c.id using cosmosAsyncContainer.queryItems. This will result in an exception during deserialization. For more details, see the code snippets in the Issue 1 and Issue 2 sections.
I have prepared a reproducer project with test cases that reflect the issues we encountered in our production code (queries) when attempting to switch from the hack involving reconfiguration of the internal ObjectMapper to using customItemSerializer.
CosmosConfig- configureENDPOINTandKEY.ReproducerTest- contains all test cases asserting our expected behavior.- Tests are categorized into two
@Nestedclasses:DateTimeSerialization- focuses on checking whether data is serialized as expected, indicating that the serializer/ObjectMapper is configured correctly.Query- contains SELECTS that fail whencustomItemSerializeris used.
- Each test includes a comment describing its purpose and how it behaves with different configuration options (see below).
- Just run the tests and review the results. The goal for us would be to have all tests green (passing) for the
BASIC_CUSTOM_SERIALIZERconfiguration option. - A test report is also available in test-report directory.
- Tests are categorized into two
Tests can be executed with four possible configuration options:
NO_CUSTOMIZATION- plainCosmosAsyncClientwithout registration ofcustomItemSerializeror reconfiguration of the internal ObjectMapper.COSMOS_INTERNALS_RECONFIGURATION– nocustomItemSerializerregistered. This is a hack involving reconfiguration of the internal Cosmos SDK ObjectMapper, which is the approach we are currently using.BASIC_CUSTOM_SERIALIZER–CosmosAsyncClientwith registration ofcustomItemSerializer. It usesObjectMapperwith required configuration for serialization/deserialization of data stored in Cosmos. However, it does not work as expected due to the issues described above.UNWRAP_CUSTOM_SERIALIZER–CosmosAsyncClientwith registration ofcustomItemSerializer. It uses anObjectMapperconfigured for serialization/deserialization of data stored in Cosmos. Additionally, it implements a hack for deserializing internalJsonSerializableobjects and unwrapping theVALUEfield as a demonstration that such a hack could work. However, this approach is very fragile and relies on internal Cosmos classes, so it is definitely not the way to use it.
Expected behavior
customItemSerializershould be called only for the conversion of the final object returned as the result of the query, not for internal Cosmos SDK structures.SqlParametershould probably usecustomItemSerializerfor parameter serialization.
Setup:
- OS: macOS 15.5 (local development environment) / Linux (production environment)
- IDE: IntelliJ
- Library/Libraries: com.azure:azure-cosmos:4.70.0
- Java version: 21.0.7
- App Server/Environment: - (not important for the issue to happen)
- Frameworks: - (not important for the issue to happen)
Information Checklist
Kindly make sure that you have added all the following information above and checkoff the required fields otherwise we will treat the issuer as an incomplete report
- Bug Description Added
- Repro Steps Added
- Setup information Added