Skip to content

@JsonUnwrapped is still broken since 2.5 #2879

@wingsofovnia

Description

@wingsofovnia
Contributor

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

Activity

wingsofovnia

wingsofovnia commented on Jan 29, 2025

@wingsofovnia
ContributorAuthor

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.

changed the title [-]`@JsonUnwrapped` is not still broken since 2.5[/-] [+]`@JsonUnwrapped` behaves differently in Kotlin since 2.6[/+] on Jan 29, 2025
changed the title [-]`@JsonUnwrapped` behaves differently in Kotlin since 2.6[/-] [+]`@JsonUnwrapped` is not still broken since 2.5[/+] on Jan 29, 2025
changed the title [-]`@JsonUnwrapped` is not still broken since 2.5[/-] [+]`@JsonUnwrapped` is still broken since 2.5[/+] on Jan 29, 2025
bnasslahsen

bnasslahsen commented on Feb 8, 2025

@bnasslahsen
Collaborator

@wingsofovnia,

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

wingsofovnia

wingsofovnia commented on Feb 8, 2025

@wingsofovnia
ContributorAuthor

@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.

added
bugSomething isn't working
and removed
invalidThis doesn't seem right
on Feb 8, 2025
bnasslahsen

bnasslahsen commented on Feb 8, 2025

@bnasslahsen
Collaborator

@wingsofovnia,

I am merging your PR now.

didjoman

didjoman commented on Feb 18, 2025

@didjoman

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 ?

3 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Participants

      @didjoman@wingsofovnia@bnasslahsen

      Issue actions

        `@JsonUnwrapped` is still broken since 2.5 · Issue #2879 · springdoc/springdoc-openapi