springdoc-openapi icon indicating copy to clipboard operation
springdoc-openapi copied to clipboard

Serialization to openapi of org.springframework.data.domain.Sort is not done correctly

Open a-locatelli opened this issue 2 years ago • 1 comments

Hi, There is something wrong with the serialization (to openapi) of the org.springframework.data.domain.Sort

I've a method which returns a org.springframework.data.domain.Page<T> from a method in a RestController class. The implementation of org.springframework.data.domain.Page<T> is org.springframework.data.domain.PageImpl<T>

@RestController
public class MyRestController {

  @PostMapping(path = "/", produces = APPLICATION_JSON_VALUE, consumes = APPLICATION_JSON_VALUE)
  @Operation(operationId="myOperation")
  public org.springframework.data.domain.Page<MyDto> queryMyDto(@RequestBody MyDtoQuery query, @ParameterObject org.springframework.data.domain.Pageable pageable) {
    // This return a PageImpl with the data, the method parameter 'query' is a pojo containg filter properties
    return myService.getResult(query, pageable); 
  }
}

The openapi defintion generated is:

"PageMyDto": {
  "type": "object",
  "properties": {
    "totalPages": {
      "type": "integer",
      "format": "int32"
    },
    "totalElements": {
      "type": "integer",
      "format": "int64"
    },
    "first": {
      "type": "boolean"
    },
    "last": {
      "type": "boolean"
    },
    "size": {
      "type": "integer",
      "format": "int32"
    },
    "content": {
      "type": "array",
      "items": {
        "$ref": "#/components/schemas/MyDto"
      }
    },
    "number": {
      "type": "integer",
      "format": "int32"
    },
    "sort": {
      "$ref": "#/components/schemas/SortObject"
    },
    "numberOfElements": {
      "type": "integer",
      "format": "int32"
    },
    "pageable": {
      "$ref": "#/components/schemas/PageableObject"
    },
    "empty": {
      "type": "boolean"
    }
  }
},
"SortObject": {
  "type": "object",
  "properties": {
    "empty": {
      "type": "boolean"
    },
    "sorted": {
      "type": "boolean"
    },
    "unsorted": {
      "type": "boolean"
    }
  }
}

When i call the method, the return object is (content is empty for this example):

http://localhost:8180/?page=0&size=20

{
  "number": 0,
  "numberOfElements": 0,
  "totalElements": 0,
  "totalPages": 0,
  "size": 20,
  "sort": [],
  "content": []
}

As you can see, the "sort" property generated is not an array, and the SortObject does not contains the Sort properties like orders.

If i set the query parameter sort, the result is:

http://localhost:8180/?page=0&size=20&sort=id

{
  "number": 0,
  "numberOfElements": 0,
  "totalElements": 0,
  "totalPages": 0,
  "size": 20,
  "sort": [
    {
      "property": "id",
      "direction": "ASC",
      "ignoreCase": false,
      "nullHandling": "NATIVE"
    }
  ],
  "content": []
}

I'm using spring boot 3.1.6 and springdoc-openapi-webmvc-ui 2.3.0.

Can you help me figure out if there is a bug or if i'm doing something wrong?

Thanks

a-locatelli avatar Dec 05 '23 16:12 a-locatelli

@a-locatelli Did you manage to solve this? Any workarounds?

Jodee90 avatar Dec 18 '23 07:12 Jodee90

@a-locatelli,

I have added a fix for it. Let me know if the fix works for you with the latest snapshot.

bnasslahsen avatar Feb 27 '24 22:02 bnasslahsen

I have the same problem, and your fix dosen't solved it.

Your excpected output is:

"sort": {
     "$ref": "#/components/schemas/SortObject"
},

But have to be:

"sort": {
  "type": "array",
  "items": {
    "$ref": "#/components/schemas/OrderObject"
  }
},

I implemented a workaround like this, with this the desired openapi.json is generated and from that working DTOs to use with Pageable and Sort.

@Configuration
class CustomOpenApiConfiguration {

    @Bean
    fun CustomSortSchemaConverter(): SortSchemaConverter {
        // Create and return the custom SortSchemaConverter
        return SortSchemaConverter()
    }
}

class SortSchemaConverter : ModelConverter {

    override fun resolve(
        type: AnnotatedType,
        context: ModelConverterContext,
        chain: MutableIterator<ModelConverter>
    ): Schema<*>? {
        val javaType = type.type

        return if (javaType is SimpleType && javaType.rawClass == Sort::class.java) {

            val orderSchema = ObjectSchema()
                .addProperty("direction", StringSchema())
                .addProperty("property", StringSchema())
                .addProperty("ignoreCase", BooleanSchema())
                .addProperty("nullHandling", StringSchema())
                .addProperty("ascending", BooleanSchema())
                .addProperty("descending", BooleanSchema())
            //  needs to be called whenever a Model is defined which can be referenced from another
            //  Model or Property
            context.defineModel("OrderObject", orderSchema)

            ArraySchema().items(
                Schema<Any>().`$ref`("#/components/schemas/OrderObject")
            )

        } else if (chain.hasNext()) {
            chain.next().resolve(type, context, chain)
        } else {
            null
        }
    }
}

SimoneFalzone avatar Feb 28 '24 06:02 SimoneFalzone

@SimoneFalzone,

Your proposed fix is too much lines of code, with too much assumptions that OrderObject schema exists. There is much simpler way to do this.

bnasslahsen avatar Feb 28 '24 11:02 bnasslahsen