-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Migrating from 3.2 to 3.8 fails if using Jackson ObjectMapper customization for Map serialization #42596
Comments
@marko-bekhta maybe something for you given you created Line 6 in 9070ad7
|
hey, yeah ... I'll take a look at this one 🙈 |
I looked at the attached example, and I'd say that a failing result is expected in this case ... [
{
"key": "hello",
"value": "world"
}
] but the app does not set up a corresponding deserializer. This means that once ORM stores the map as an array of objects it cannot deserialize this array back to the map... |
I'll give that a try, thanks. Though I'm not sure I understand why there is a deserialization happening in this test case - why does the JSON need to be deserialized somewhere deeper in the call stack after |
Also, sorry, but with a Having to implement a deserializer on the other side - when the application isn't even actually trying to deserialize these entities directly - is also a bit painful because we're running into Java generics and type erasure. I guess what I am surprised about here is that the Jackson customization is actually being applied at the ORM layer. It doesn't matter to me if the JSON column in this Postgres db table is serialized with this customization or not, all that matters to me is the REST API response format. Is it no longer possible to customize only that serialization format? |
I will have to refresh my memory but at some point we discussed the ability to customize the ObjectMapper for a specific purpose. I think having a global one that is used everywhere might become problematic, especially for ORM. Let’s say you change the way something is serialized/deserialized for your REST services and all of a sudden you are unable to deserialize what you pushed to your database. I think we need to discuss this. |
And in the case of ORM, I think we shouldn’t copy the default one: we should have a specific one that you can customize. Now the big problem is that it’s going to be a massive breaking change… |
hmmm... we had this one #32029 and I think the idea there was to use the default object mapper, but have those
But then ... yeah if something changes in serialization, then retrieving existing entities may result in errors ... I guess then the |
Yeah problem is the global object mapper is used as is to customize the REST services output. And then it cascades all over the place, which is probably not what you want when dealing with very specific use-cases such as saving data to your database. |
In an ideal world:
Problem in #32029 was that the ObjectMapper was badly configured in the first place and then you probably want to be able to customize it. But the fact that you have to tweak the default ObjectMapper for the server usage and cannot apply things only to REST server is a problem. I think our only way out while not breaking existing applications would be to:
Document this extremely carefully. @andrewazores would this approach works for you? |
sooo we need to add a customization of a REST server mapper, and what was done to customize the mapper in this issue would be moved from a global customization to this new thing. We already can customize the global mapper, the one for ORM and the one for a REST client ... Or I'm missing something 🙈 |
@gsmet I think that would work. The shape of the POJO is one thing, and custom serialization formats are a contextual thing, so it makes sense to have different formatters or ObjectMappers for different purposes. Even a simple transformation like changing a timestamp format from ISO-8601 to epoch seconds would be another similar example where this concept applies. Maybe the database has epoch seconds, I want my API to send ISO-8601 out, and I have a REST client talking to a Golang service where I want to use Go's standard format. Regarding this being a breaking change: I understand and empathize 🙂 I discovered this to begin with because this is already a breaking change from 3.2->3.8. This reproducer is a boiled-down example of a real thing in Cryostat. |
@gsmet @marko-bekhta any further thoughts on this? I'd mostly like to know:
|
Hey @andrewazores Yes, we still want to address this; it's just that we haven't had time to get back to it yet. (not sure into which version the fix will get into though ... ) |
I'm not sure if I understand. Do you mean to do something like this to the reproducer? diff --git a/pom.xml b/pom.xml
index 4246acf..0bceaac 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,15 +6,15 @@
<version>1.0.0-SNAPSHOT</version>
<properties>
- <compiler-plugin.version>3.11.0</compiler-plugin.version>
+ <compiler-plugin.version>3.12.1</compiler-plugin.version>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
- <quarkus.platform.version>3.2.12.Final</quarkus.platform.version>
+ <quarkus.platform.version>3.8.6</quarkus.platform.version>
<skipITs>true</skipITs>
- <surefire-plugin.version>3.0.0</surefire-plugin.version>
+ <surefire-plugin.version>3.2.3</surefire-plugin.version>
</properties>
<dependencyManagement>
diff --git a/src/main/java/org/acme/HibernateMapperCustomization.java b/src/main/java/org/acme/HibernateMapperCustomization.java
new file mode 100644
index 0000000..944492f
--- /dev/null
+++ b/src/main/java/org/acme/HibernateMapperCustomization.java
@@ -0,0 +1,35 @@
+package org.acme;
+
+import org.hibernate.type.descriptor.WrapperOptions;
+import org.hibernate.type.descriptor.java.JavaType;
+import org.hibernate.type.format.FormatMapper;
+import org.hibernate.type.format.jackson.JacksonJsonFormatMapper;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import io.quarkus.hibernate.orm.PersistenceUnitExtension;
+import jakarta.inject.Inject;
+
+@JsonFormat
+@PersistenceUnitExtension
+public class HibernateMapperCustomization implements FormatMapper {
+
+ JacksonJsonFormatMapper delegate;
+
+ @Inject
+ HibernateMapperCustomization(ObjectMapper objectMapper) {
+ this.delegate = new JacksonJsonFormatMapper(objectMapper);
+ }
+
+ @Override
+ public <T> T fromString(CharSequence charSequence, JavaType<T> javaType, WrapperOptions wrapperOptions) {
+ return delegate.fromString(charSequence, javaType, wrapperOptions);
+ }
+
+ @Override
+ public <T> String toString(T value, JavaType<T> javaType, WrapperOptions wrapperOptions) {
+ return delegate.toString(value, javaType, wrapperOptions);
+ }
+
+} I tried this just now and it still results in a deserialization failure:
So it looks like in any case, whether it's the "global" customizer or the Hibernate-specific one, I would need to implement a deserializer to handle the data coming back out of the database, because in Quarkus 3.7+ this key-value customization I want to apply can only be applied in such a way that it also ends up affecting Hibernate. But then, like I was saying before, now I have to try to parse all @gsmet 's idea of having a global ObjectMapper, a REST client ObjectMapper customization (which already exists), a REST server customization, and a persistence customization (or per-unit customizations) sounds like it would be perfect. |
almost 🙈 😃... before the introduction of the In your example, you are trying to inject the object mapper, which will be customized with your serializer.... so instead create a clean mapper (without any custom serialization you are doing for REST side) and pass that one to the delegate: @JsonFormat
@PersistenceUnitExtension
public class HibernateMapperCustomization implements FormatMapper {
JacksonJsonFormatMapper delegate;
HibernateMapperCustomization() {
this.delegate = new JacksonJsonFormatMapper( new ObjectMapper().findAndRegisterModules() );
}
@Override
public <T> T fromString(CharSequence charSequence, JavaType<T> javaType, WrapperOptions wrapperOptions) {
return delegate.fromString(charSequence, javaType, wrapperOptions);
}
@Override
public <T> String toString(T value, JavaType<T> javaType, WrapperOptions wrapperOptions) {
return delegate.toString(value, javaType, wrapperOptions);
}
} this way the serialization/deserialization in ORM should happen exactly as before in 3.2... |
yes, indeed 👍🏻 we just need to get to work on it 🙈 😕 |
Thanks for the pointer @marko-bekhta , I'm trying that approach now... diff --git a/pom.xml b/pom.xml
index 4246acf..1e12433 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,7 +12,7 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
- <quarkus.platform.version>3.2.12.Final</quarkus.platform.version>
+ <quarkus.platform.version>3.8.6</quarkus.platform.version>
<skipITs>true</skipITs>
<surefire-plugin.version>3.0.0</surefire-plugin.version>
</properties>
diff --git a/src/main/java/org/acme/HibernateMapperCustomization.java b/src/main/java/org/acme/HibernateMapperCustomization.java
new file mode 100644
index 0000000..b699853
--- /dev/null
+++ b/src/main/java/org/acme/HibernateMapperCustomization.java
@@ -0,0 +1,41 @@
+package org.acme;
+
+import org.hibernate.type.descriptor.WrapperOptions;
+import org.hibernate.type.descriptor.java.JavaType;
+import org.hibernate.type.format.FormatMapper;
+import org.hibernate.type.format.jackson.JacksonJsonFormatMapper;
+import org.jboss.logging.Logger;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import io.quarkus.hibernate.orm.PersistenceUnitExtension;
+import jakarta.inject.Inject;
+
+@JsonFormat
+@PersistenceUnitExtension
+public class HibernateMapperCustomization implements FormatMapper {
+
+ JacksonJsonFormatMapper delegate;
+ Logger logger;
+
+ @Inject
+ HibernateMapperCustomization(Logger logger) {
+ logger.info("Created customizer");
+ this.delegate = new JacksonJsonFormatMapper(new ObjectMapper().findAndRegisterModules());
+ this.logger = logger;
+ }
+
+ @Override
+ public <T> T fromString(CharSequence charSequence, JavaType<T> javaType, WrapperOptions wrapperOptions) {
+ logger.info("fromString");
+ return delegate.fromString(charSequence, javaType, wrapperOptions);
+ }
+
+ @Override
+ public <T> String toString(T value, JavaType<T> javaType, WrapperOptions wrapperOptions) {
+ logger.info("toString");
+ return delegate.toString(value, javaType, wrapperOptions);
+ }
+
+} and then Confusingly, I don't see any logger output indicating that this customization is even instantiated, let alone that it gets called into. I get the same |
should be a |
🤦 thank you @marko-bekhta , that did the trick for the reproducer! I'll apply this technique to the actual project and make sure it all turns out as expected too, but I think this unblocks me :-) |
It looks like that workaround does work for my current needs. Thanks for all the help and explanation @marko-bekhta ! |
Happy to help and glad it worked for you! 😃 |
Describe the bug
See reproducer here: https://github.com/andrewazores/quarkus-jackson-map-format
Using Jackson customization as per https://quarkus.io/guides/rest-json#json.
When using Quarkus 3.2, this simple reproducer works as expected:
That is, the
Map<String, String> labels
field of the Model gets serialized into a list of key-value objects.When upgrading to the next LTS (3.8), I would hope that this still works.
Expected behavior
Doing
quarkus update -S 3.8
and repeating the reproducer test should yield the same result.Actual behavior
After doing
quarkus update -S 3.8
and repeating the test, there are exceptions:The devserver stack trace is more readable of course:
How to Reproduce?
See reproducer here: https://github.com/andrewazores/quarkus-jackson-map-format
quarkus dev
http -f :8080/models name=foo
quarkus update -S 3.8
quarkus dev
http -f :8080/models name=foo
Output of
uname -a
orver
No response
Output of
java -version
openjdk version "17.0.12" 2024-07-16 OpenJDK Runtime Environment (Red_Hat-17.0.12.0.7-2) (build 17.0.12+7) OpenJDK 64-Bit Server VM (Red_Hat-17.0.12.0.7-2) (build 17.0.12+7, mixed mode, sharing)
Quarkus version or git rev
3.2.12 , 3.8.5
Build tool (ie. output of
mvnw --version
orgradlew --version
)3.9.1
Additional information
It seems that in 3.2, the ObjectMapper instance that I am customizing is either a different instance, or is not used by Panache/Hibernate. Whereas in 3.8, the customization I apply (which I want for the purposes of my REST API and the format desired by the API client) becomes incorrectly applied to some other internals.
The text was updated successfully, but these errors were encountered: