-
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
Use generic return type of method to construct proper Jackson writer #43700
Conversation
danielbobbert
commented
Oct 4, 2024
•
edited by geoand
Loading
edited by geoand
- Fixes: Quarkus REST fails to write polymorphic type property in JSON #43631
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for this PR and all the detective work.
Added a small suggestion and might be a good idea to also avoid the lambda if we can as we try to reduce their usage to the bare minimum in Quarkus runtime code.
return this.defaultWriter; | ||
} else { | ||
// compute and cache a specific writer for the given generic type | ||
return genericWriters.computeIfAbsent(genericType, type -> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For CHM, it's usually recommended to do a get first when getting a hit is the most common pattern as I was told computeIfAbsent
locks were a bit less optimized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- lambda removed
- get() before computeIfAbsent() added
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am still not 100% happy with the solution, because I feel I am generating too many custom writers (basically one per different method return type), even though that's probably only necessary for methods that return generic collections etc. (basically all those methods where `method.getGenericReturnType() <> method.getReturnType()). For methods with non-generic return types, the default writer works correctly.
I will dig just a little bit deeper to see whether I can differentiate those cases and only generate a custom writer if it is really necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have checked the details of how and why Jackson works this way and performed various other tests. As commented in the code, Jackson needs that custom writer exactly in those cases, where the target type is a parameterized type.
So I have updated the code once more to compute a specialized writer only in those cases, where the method return type actually is a generic (parameterized) type. In all other cases, the default writer can be used.
Imho, the code is good to go.
BTW: I also compared my solution to what is being done in FullyFeaturedServerJacksonMessageBodyWriter (which computes a customer writer per method/type as well). One difference that I spotted is that FullyFeaturedServerJacksonMessageBodyWriter maps the type by name (perTypeWriter in line 40) instead of using the type itself as the map key. I don't see, why this would be beneficial (on the contrary: computing the type name every time should be more expensive that using hashCode() and equals() of the type itself, right?!), but please correct me if I'm wrong.
ecf71fc
to
ea589e4
Compare
@danielbobbert just a heads up that we are at Devoxx this week so we will probably have a closer look next week! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
This comment has been minimized.
This comment has been minimized.
The rest failures seem related |
Yes. The problem is that the "genericType" argument that is passed to the serializer is not properly unwrapped for CompletableFuture. When a method returns a Uni, the object "o" passed to writeResponse() is an "X" and the "genericType" is "X.class" (and not Uni). But for a return type of "CompletableFuture", the "o" is of type "X" (so the future has been resolved and the actual value is passed to serialization) but the "genericType" is still "CompletableFuture". Because it is generic, my new code selects a special serializer, but of course that fails, because a serializer for "CompletableFuture" cannot serialize an "X". Now, I could of course add special handling for that case, but I'd rather find out, why the genericType is properly unwrapped for Unis but not for CompletableFutures (and maybe other types such as CompletionStage, Future, etc. as well?!) |
I just verified that it does work properly for methods returning CompletionStage. |
Ah, nice find. I'll have to track down where the unwrapping is done as I don't remember of the top of my head. |
Found it. It's in org.jboss.resteasy.reactive.common.util.types.Types.getEffectiveReturnType() |
Yup, just found that as well. Feel free to push to the change to that method |
Line 185 should probably be changed from Do you want me to add that to my PR and give it a try? |
Please do |
Just rebased, amended and pushed the change |
This comment has been minimized.
This comment has been minimized.
0fe61ce
to
1fccf49
Compare
OK, two problems: |
Can you elaborate a little more on this? Thanks |
Some tests in HalLinksWithJacksonTest were failing. I figured that's because the HalServerResponseFilter wraps the actual entity returned by the method into a HalEntityWrapper (or HalCollectionWrapper). |
This comment has been minimized.
This comment has been minimized.
That's odd. IIRC, CI should just run on forks without any necessary intervention on your part.
Try building the entire project with:
and then running a test like so:
|
|
||
abstract class ServerJacksonMessageBodyWriter extends ServerMessageBodyWriter.AllWriteableMessageBodyWriter { | ||
|
||
ObjectWriter getGenericWriter(Type genericType, ObjectWriter defaultWriter) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing the BodyWriters hierarchy only to add this method seems a bit unnecessary. Actually this method could be a static
one and moved to JacksonMapperUtil
that already contains a collection of these static utility methods necessary for serialization, thus eliminating this intermediate class in the writers' hierarchy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I will move the method and also check on those failing tests. Seems like I am still missing a tiny bit...
e8ce996
to
5cda62d
Compare
ObjectWriter writer = genericWriters.get(genericType); | ||
if (writer == null) { | ||
// no cached writer for that type. Compute it. | ||
writer = JacksonMapperUtil.getGenericWriter(genericType, this.defaultWriter); | ||
genericWriters.put(genericType, writer); | ||
} | ||
return writer; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This operation is not atomic and it may happen that multiple writers could be unnecessarily generated for the same type. I believe that this whole block could be replaced with a one-liner, something like:
return genericWriters.computeIfAbsent(genericType, type -> JacksonMapperUtil.getGenericWriter(type, defaultWriter));
64b0966
to
c7a3de7
Compare
OK, so I have double checked with resteasy-classic to solve the problem in the same way as it was done there. Moved the method that derives the appropriate root type to JacksonMapperUtil and call that from both Basic- and FullFeaturedServerJacksonResponseWriter. This should now pass all tests! Unfortunately, CI still isn't working for me on my fork. But local execution of tests was possible after some fiddling. For some strange reason, tests will not launch correctly every now and then with nonsense ServiceProvider exceptions such as
|
We should also keep in mind that this might have a small performance implication. but the change is necessary in any case |
Status for workflow
|
I think it's worth pushing it to 3.16.0, I added the backport label. |
Good idea! |
@danielbobbert thanks for all your work on this! |