Skip to content

@JsonUnwrapped is still broken since 2.5 #2879

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

Closed
wingsofovnia opened this issue Jan 29, 2025 · 7 comments · Fixed by #2894 or #2909
Closed

@JsonUnwrapped is still broken since 2.5 #2879

wingsofovnia opened this issue Jan 29, 2025 · 7 comments · Fixed by #2894 or #2909
Labels
bug Something isn't working

Comments

@wingsofovnia
Copy link
Contributor

wingsofovnia commented Jan 29, 2025

Describe the bug

Since @JsonUnwrapped has been broken in 2.6.0 I still not able to make @JsonUnwrapped work with my Kotlin data classes despite numerous fixes:

To Reproduce

data class ContactCreateRequest(
    @JsonUnwrapped
    val person: Person,

    @Schema(required = false)
    @JsonProperty("labels", required = false)
    val labels: List<String>? = null,
)

data class Person(
    @Schema(required = true, example = "Oscar Claude Monet")
    @JsonProperty("name", required = true)
    val name: String,

    @Schema(example = "+38051231412")
    @JsonProperty("phone")
    val phone: String? = null,

    @Schema(required = true, example = "oscar.monet@lafrance.fr")
    @JsonProperty(value = "email", required = true)
    val email: String,
)

This example in a sample Spring Boot project:

Dependencies

  • Kotlin 2.1.10
  • org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.4
  • org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.4
  • org.springframework.boot:spring-boot-....:3.4.2

Expected Behavior (v2.5.0)
Image

Actual Behavior (v2.8.4)
Image

@wingsofovnia
Copy link
Contributor Author

wingsofovnia commented Jan 29, 2025

I did some research and figured out that while this does not work:

data class ContactCreateRequest(
	@JsonUnwrapped
	val person: Person,
)

this does:

data class ContactCreateRequest(
	@field:JsonUnwrapped // Apply @JsonUnwrapped to field
	val person: Person,
)

The decompiled byte code for those look like that:

public final class ContactCreateRequest {
   @NotNull
   private final Person person;

   public ContactCreateRequest(@JsonUnwrapped @NotNull Person person) {
      Intrinsics.checkNotNullParameter(person, "person");
      super();
      this.person = person;
   }
   
   @NotNull
   public final Person getPerson() {
      return this.person;
   }
// ....
}

vs

public final class ContactCreateRequest {
   @JsonUnwrapped
   @NotNull
   private final Person person;

   public ContactCreateRequest(@NotNull Person person) {
      Intrinsics.checkNotNullParameter(person, "person");
      super();
      this.person = person;
   }
   
   @NotNull
   public final Person getPerson() {
      return this.person;
   }
// ....
}

Funny thing is that if I try to replicate the first approach with pure Java, @JsonCreators with @JsonUnwrapped are not supported yet:

public class Book {
    private final Author author;
    private final String title;

    public Book(@JsonUnwrapped(prefix = "author_") Author author, @JsonProperty("title") String title) {
        this.author = author;
        this.title = title;
    }
}
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Invalid type definition for type `com.example.demo.Book`: 
   Cannot define Creator parameter 0 as `@JsonUnwrapped`: combination not yet supported

This must be the Kotlin plugin doing some magic on top of default Jackson. This however is expected to change in the next Jackson minor release with FasterXML/jackson-databind#1467 merge.

Given that in this example ContactCreateRequest used for request body, it is deserialisation "view" that should be considered. It looks like the lookup for @JsonUnwrapped since 2.5.0 has been reduced to looking at field annotations only.
Both for Kotlin support and upcoming support of creators + unwrapped, I believe springdoc should bring the support back at looking for @JsonUnwrapped in creators and should distinguish between deserialisation and serialisation configurations.

@wingsofovnia wingsofovnia changed the title @JsonUnwrapped is not still broken since 2.5 @JsonUnwrapped behaves differently in Kotlin since 2.6 Jan 29, 2025
@wingsofovnia wingsofovnia changed the title @JsonUnwrapped behaves differently in Kotlin since 2.6 @JsonUnwrapped is not still broken since 2.5 Jan 29, 2025
@wingsofovnia wingsofovnia changed the title @JsonUnwrapped is not still broken since 2.5 @JsonUnwrapped is still broken since 2.5 Jan 29, 2025
@bnasslahsen
Copy link
Collaborator

bnasslahsen commented Feb 8, 2025

@wingsofovnia,

There is no code in Springdoc responsible for JsonUnwrapped causing this behavior.
Looks like i will count on your deeper analysis!

@bnasslahsen bnasslahsen added the invalid This doesn't seem right label Feb 8, 2025
@wingsofovnia
Copy link
Contributor Author

wingsofovnia commented Feb 8, 2025

@bnasslahsen there is though. I am afraid this is a bug in PolymorphicModelConverter#resolve.

I've just replicated the issue on 2.8.4. This condiiton:

for (Field field : FieldUtils.getAllFields(javaType.getRawClass())) {
if (field.isAnnotationPresent(JsonUnwrapped.class)) {
if (!TypeNameResolver.std.getUseFqn())
PARENT_TYPES_TO_IGNORE.add(javaType.getRawClass().getSimpleName());

Is not going through because the annotation is on the getter, which is valid placement of this annotation too. Once I force this via debugger:


The type is properly unwrapped.

Please re-open the ticket.

@bnasslahsen bnasslahsen reopened this Feb 8, 2025
@bnasslahsen bnasslahsen added bug Something isn't working and removed invalid This doesn't seem right labels Feb 8, 2025
@bnasslahsen
Copy link
Collaborator

@wingsofovnia,

I am merging your PR now.

@didjoman
Copy link

Hi,
I have an issue with the fix.

If you have a class like the next one, then the isUnwrapped boolean in the PolymorphicModelConverter is now false, but it was true before :

public final class ContactCreateRequest {

   @JsonProperty(access = JsonProperty.Access.READ_ONLY)
   @JsonUnwrapped
   private final Person person;

   public ContactCreateRequest(Person person) {
      this.person = person;
   }
   
   public final Person getPerson() {
      return this.person;
   }
}

I am talking about this line:
Before, isUnwrapped was true:

for (Field field : FieldUtils.getAllFields(javaType.getRawClass())) {
if (field.isAnnotationPresent(JsonUnwrapped.class)) {

Now, isUnwrapped is false:

BeanDescription javaTypeIntrospection = springDocObjectMapper.jsonMapper().getDeserializationConfig().introspect(javaType);
for (BeanPropertyDefinition property : javaTypeIntrospection.findProperties()) {
boolean isUnwrapped = (property.getField() != null && property.getField().hasAnnotation(JsonUnwrapped.class)) ||
(property.getGetter() != null && property.getGetter().hasAnnotation(JsonUnwrapped.class));

Wouldn't there be an other way to get the annotation ?

@wingsofovnia
Copy link
Contributor Author

Thank you, @didjoman, for reporting. Filed #2909 to fix it.

On that note, are you using read only / write only fields extensively for GET/POST requests? I couldn't find any test that checks for readOnly: true or writeOnly: true. Is it supported by springdoc at all?

@wingsofovnia
Copy link
Contributor Author

@didjoman #2909 merged, you should be all good with the upcoming patch or minor version. Thank you for the report!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
3 participants