Skip to content
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

Unwrap Kotlin inline value classes return values #33026

Closed
serandel opened this issue Jun 14, 2024 · 7 comments
Closed

Unwrap Kotlin inline value classes return values #33026

serandel opened this issue Jun 14, 2024 · 7 comments
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) theme: kotlin An issue related to Kotlin support type: bug A general bug
Milestone

Comments

@serandel
Copy link

serandel commented Jun 14, 2024

Affects: Spring 6.1.6


REST controllers are unable to return Kotlin value classes, either directly or wrapped in a ResponseEntity.

I'm using extensively value classes as a way of defining my domain functionally, but have found what I think it's a Spring MVC bug.

Minimal Proof of Concept:

@Serializable
@JvmInline
value class Foo(val value: String)

@GetMapping(
    value = ["/foo"],
    produces = [MediaType.TEXT_PLAIN_VALUE],
)
fun getFoo(): Foo = Foo("bar")

Expected result:

% curl localhost:8080/foo
bar

Actual result:

% curl localhost:8080/foo
{"type":"about:blank","title":"Internal Server Error","status":500,"detail":"Failed to write request","instance":"/foo"}

Workaround: just unwrap the inner value at the controller level before returning

@GetMapping(
    value = ["/foo"],
    produces = [MediaType.TEXT_PLAIN_VALUE],
)
fun getFoo(): String = Foo("bar").value

What's interesting to me is that I have no problem with value classes as input parameters, either in a RequestBody, a PathVariable, etc. They work seamlessly.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jun 14, 2024
@snicoll
Copy link
Member

snicoll commented Jun 14, 2024

Minimal Proof of Concept:

Please move that to something we can actually run. It's too bare bone and anything that's missing is a guess on our end. Also, The actual result is pretty useless since we don't have the actual stacktrace that was thrown.

@snicoll snicoll added status: waiting-for-feedback We need additional information before we can continue theme: kotlin An issue related to Kotlin support labels Jun 14, 2024
@sdeleuze sdeleuze added the in: web Issues in web modules (web, webmvc, webflux, websocket) label Jun 14, 2024
@sdeleuze sdeleuze self-assigned this Jun 14, 2024
@sdeleuze sdeleuze added type: regression A bug that is also a regression and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Jun 17, 2024
@sdeleuze sdeleuze added this to the 6.1.10 milestone Jun 17, 2024
@sdeleuze
Copy link
Contributor

This looks a regression due to the fact that now all Kotlin function invocations are handled by Kotlin reflection to support various use cases (parameters with default values for example). For some reason, Kotlin reflective invocation via kotlin.reflect.KCallable#callBy return the wrapped value (Foo), not the unwrapped one (String). That should be fixed either on Spring or Kotlin side. I have asked a feedback from the Kotlin team.

@sdeleuze sdeleuze removed the status: waiting-for-feedback We need additional information before we can continue label Jun 17, 2024
@sdeleuze sdeleuze modified the milestones: 6.1.10, 6.2.0-M5 Jun 17, 2024
@sdeleuze sdeleuze added status: waiting-for-internal-feedback An issue that needs input from a member or another Spring Team and removed type: regression A bug that is also a regression labels Jun 18, 2024
@sdeleuze sdeleuze removed this from the 6.2.0-M5 milestone Jun 18, 2024
@sdeleuze
Copy link
Contributor

sdeleuze commented Jun 18, 2024

After spending some time digging into this issue, I can say it is more nuanced and complex that what I thought originally.

As far as I can tell, inline value classes are handled correctly but are interpreted unwrapped since Kotlin reflection handle it that way by design, that's why when you want to treat @JvmInline value class Foo(val value: String) as String it fails. But without produces = [MediaType.TEXT_PLAIN_VALUE] with Jackson, it will serialize as "bar" as expected for the unwrapped serialization.

The use case that is really broken is the serialization by Kotlin Serialization (which is inline value class aware) where there is a mismatch between the Java type of the return value (String) and the type of the value to serialize (Foo). This should be fixed via #33016.

I made some try handling the Foo to String conversion on Spring side, but handling that for all use cases (plain return value, ResponseEntity<T> and suspending functions) on both WebMVC and WebFlux is very tricky and fragile. So for now, I think I am on the line of:

I will discuss that with the wider team and share the result of the discussion here.

@sdeleuze sdeleuze added type: documentation A documentation task and removed status: waiting-for-internal-feedback An issue that needs input from a member or another Spring Team labels Jun 18, 2024
@sdeleuze sdeleuze added this to the 6.1.10 milestone Jun 18, 2024
@sdeleuze
Copy link
Contributor

sdeleuze commented Jun 18, 2024

@serandel The team agreed with my proposal, so I keep that issue in the backlog and create another documentation issue to document current behavior and will work on fixing the Kotlin Serialization use case via #33016. The behavior you hope will likely require #21546 which will take more time to be fixed.

@sdeleuze sdeleuze changed the title Returning a Kotlin value class from a REST controller fails Document Kotlin inline value classes return value behavior Jun 18, 2024
@sdeleuze sdeleuze added type: enhancement A general enhancement and removed type: documentation A documentation task labels Jun 18, 2024
@sdeleuze sdeleuze modified the milestones: 6.1.10, General Backlog Jun 18, 2024
@sdeleuze sdeleuze changed the title Document Kotlin inline value classes return value behavior Unwrap Kotlin inline value classes return values Jun 18, 2024
@sdeleuze
Copy link
Contributor

sdeleuze commented Jun 18, 2024

Hum, based on the additional tests I did, it looks like we still have an inconsistency to fix between body, valueType and bodyType in AbstractMessageConverterMethodProcessor#writeWithMessageConverters
image in in order to have a consistent behavior which would the outcome I would like to have with Spring Framework 6.2. I will share an update once #33016 is fixed.

@serandel
Copy link
Author

Awesome work here, @sdeleuze, thanks a lot.

I didn't have the time till now, but I take it you already have a better PoC than anything I could extract from my original project, right? Just ping me if I can do anything to help.

@sdeleuze sdeleuze modified the milestones: General Backlog, 6.2.0-M5 Jul 10, 2024
@sdeleuze
Copy link
Contributor

That's ok, I have built a similar repro and I think I have found a way to fix this.

@sdeleuze sdeleuze added type: bug A general bug and removed type: enhancement A general enhancement labels Jul 10, 2024
@sdeleuze sdeleuze modified the milestones: 6.2.0-M5, 6.1.11 Jul 10, 2024
sdeleuze added a commit to sdeleuze/spring-framework that referenced this issue Jul 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) theme: kotlin An issue related to Kotlin support type: bug A general bug
Projects
None yet
Development

No branches or pull requests

4 participants