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

Schema replaced by String when using @ApiResponse with RepresentationModel (Hateoas links) #2902

Closed
didjoman opened this issue Feb 12, 2025 · 7 comments
Labels
bug Something isn't working

Comments

@didjoman
Copy link

didjoman commented Feb 12, 2025

Describe the bug

When using the annotation @ApiResponse with a content and a mediatype, the schema of my response object is being replaced by a String (instead of the reference to the actual schema).

Example:

    @GetMapping("/test")
    @ResponseStatus(HttpStatus.OK)
    @Operation(summary = "get", description = "Provides a response.")
    @ApiResponse(content = @Content(mediaType = MediaTypes.HAL_JSON_VALUE,
        schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = Response.class)),
        responseCode = "200")
    public Response get() {
        return new Response("value");
    }

This is due to a NullPointerException occuring in the new HateoasLinksConverter.
The stacktrace (not displayed as catched in SpringDocAnnotationsUtils line 442) :

        java.lang.NullPointerException: Cannot invoke "String.substring(int)" because the return value of "io.swagger.v3.oas.models.media.Schema.get$ref()" is null
	at org.springdoc.core.converters.HateoasLinksConverter.resolve(HateoasLinksConverter.java:74)
	at org.springdoc.core.converters.PageableOpenAPIConverter.resolve(PageableOpenAPIConverter.java:97)
	at org.springdoc.core.converters.PageOpenAPIConverter.resolve(PageOpenAPIConverter.java:102)
	at org.springdoc.core.converters.SortOpenAPIConverter.resolve(SortOpenAPIConverter.java:92)
	at io.swagger.v3.core.converter.ModelConverterContextImpl.resolve(ModelConverterContextImpl.java:97)
	at io.swagger.v3.core.jackson.ModelResolver.resolve(ModelResolver.java:493)
	at org.springdoc.core.converters.WebFluxSupportConverter.resolve(WebFluxSupportConverter.java:89)
	at org.springdoc.core.converters.AdditionalModelsConverter.resolve(AdditionalModelsConverter.java:163)
	at org.springdoc.core.converters.FileSupportConverter.resolve(FileSupportConverter.java:72)
	at org.springdoc.core.converters.ResponseSupportConverter.resolve(ResponseSupportConverter.java:84)
	at org.springdoc.core.converters.SchemaPropertyDeprecatingConverter.resolve(SchemaPropertyDeprecatingConverter.java:95)
	at org.springdoc.core.converters.PolymorphicModelConverter.resolve(PolymorphicModelConverter.java:141)
	at org.springdoc.core.converters.OAS31ModelConverter.resolve(OAS31ModelConverter.java:49)
	at org.springdoc.core.converters.CollectionModelContentConverter.resolve(CollectionModelContentConverter.java:84)
	at org.springdoc.core.converters.HateoasLinksConverter.resolve(HateoasLinksConverter.java:87)
	at org.springdoc.core.converters.PageableOpenAPIConverter.resolve(PageableOpenAPIConverter.java:97)
	at org.springdoc.core.converters.PageOpenAPIConverter.resolve(PageOpenAPIConverter.java:102)
	at org.springdoc.core.converters.SortOpenAPIConverter.resolve(SortOpenAPIConverter.java:92)
	at io.swagger.v3.core.converter.ModelConverterContextImpl.resolve(ModelConverterContextImpl.java:97)
	at io.swagger.v3.core.jackson.ModelResolver.resolve(ModelResolver.java:745)
	at org.springdoc.core.converters.WebFluxSupportConverter.resolve(WebFluxSupportConverter.java:89)
	at org.springdoc.core.converters.AdditionalModelsConverter.resolve(AdditionalModelsConverter.java:163)
	at org.springdoc.core.converters.FileSupportConverter.resolve(FileSupportConverter.java:72)
	at org.springdoc.core.converters.ResponseSupportConverter.resolve(ResponseSupportConverter.java:84)
	at org.springdoc.core.converters.SchemaPropertyDeprecatingConverter.resolve(SchemaPropertyDeprecatingConverter.java:95)
	at org.springdoc.core.converters.PolymorphicModelConverter.resolve(PolymorphicModelConverter.java:141)
	at org.springdoc.core.converters.OAS31ModelConverter.resolve(OAS31ModelConverter.java:49)
	at org.springdoc.core.converters.CollectionModelContentConverter.resolve(CollectionModelContentConverter.java:84)
	at org.springdoc.core.converters.HateoasLinksConverter.resolve(HateoasLinksConverter.java:73)
	at org.springdoc.core.converters.PageableOpenAPIConverter.resolve(PageableOpenAPIConverter.java:97)
	at org.springdoc.core.converters.PageOpenAPIConverter.resolve(PageOpenAPIConverter.java:102)
	at org.springdoc.core.converters.SortOpenAPIConverter.resolve(SortOpenAPIConverter.java:92)
	at io.swagger.v3.core.converter.ModelConverterContextImpl.resolve(ModelConverterContextImpl.java:97)
	at io.swagger.v3.core.converter.ModelConverters.resolveAsResolvedSchema(ModelConverters.java:192)
	at org.springdoc.core.utils.SpringDocAnnotationsUtils.extractSchema(SpringDocAnnotationsUtils.java:137)
	at org.springdoc.core.service.GenericResponseService.calculateSchema(GenericResponseService.java:563)
	at org.springdoc.core.service.GenericResponseService.buildContent(GenericResponseService.java:541)
	at org.springdoc.core.service.GenericResponseService.buildContent(GenericResponseService.java:522)
	at org.springdoc.core.service.GenericResponseService.buildApiResponses(GenericResponseService.java:595)
	at org.springdoc.core.service.GenericResponseService.buildApiResponses(GenericResponseService.java:469)
	at org.springdoc.core.service.GenericResponseService.build(GenericResponseService.java:264)
	at org.springdoc.api.AbstractOpenApiResource.calculatePath(AbstractOpenApiResource.java:626)
	at org.springdoc.api.AbstractOpenApiResource.calculatePath(AbstractOpenApiResource.java:816)
	at org.springdoc.webmvc.api.OpenApiResource.lambda$calculatePath$11(OpenApiResource.java:222)
	at java.base/java.util.Optional.ifPresent(Optional.java:178)
	at org.springdoc.webmvc.api.OpenApiResource.calculatePath(OpenApiResource.java:203)
	at org.springdoc.webmvc.api.OpenApiResource.lambda$getPaths$2(OpenApiResource.java:173)
	at java.base/java.util.Optional.ifPresent(Optional.java:178)
	at org.springdoc.webmvc.api.OpenApiResource.getPaths(OpenApiResource.java:152)
	at org.springdoc.api.AbstractOpenApiResource.getOpenApi(AbstractOpenApiResource.java:370)
	at org.springdoc.webmvc.api.OpenApiResource.openapiJson(OpenApiResource.java:127)
	at org.springdoc.webmvc.api.OpenApiWebMvcResource.openapiJson(OpenApiWebMvcResource.java:117)

How to reproduce

git clone https://github.com/didjoman/Springdoc-String-Schema-returned-mediatype-issue
Run the application (spring boot).
Run curl --location --request GET 'http://localhost:8080/api-docs' --header 'Content-Type: application/json'

Expected behavior

I expect to have the same OAS generated as before v2.8.2, that is to say:

{
  "openapi": "3.1.0",
  "info": {
    "title": "OpenAPI definition",
    "version": "v0"
  },
  "servers": [
    {
      "url": "http://localhost:8080",
      "description": "Generated server url"
    }
  ],
  "paths": {
    "/test": {
      "get": {
        "tags": [
          "basic-controller"
        ],
        "summary": "get",
        "description": "Provides a response.",
        "operationId": "get",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/hal+json": {
                "schema": {
                  "$ref": "#/components/schemas/Response"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Link": {
        "type": "object",
        "properties": {
          "href": {
            "type": "string"
          },
          "hreflang": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "deprecation": {
            "type": "string"
          },
          "profile": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "templated": {
            "type": "boolean"
          }
        }
      },
      "Response": {
        "type": "object",
        "description": "test description",
        "properties": {
          "value2": {
            "type": "string"
          },
          "_links": {
            "$ref": "#/components/schemas/Links"
          }
        }
      },
      "Links": {
        "type": "object",
        "additionalProperties": {
          "$ref": "#/components/schemas/Link"
        }
      }
    }
  }
}

Actual behavior

Since v2.8.2 I have this, the Response schema is not generated and its reference in the path is replaced by the type String:

{
  "openapi": "3.1.0",
  "info": {
    "title": "OpenAPI definition",
    "version": "v0"
  },
  "servers": [
    {
      "url": "http://localhost:8080",
      "description": "Generated server url"
    }
  ],
  "paths": {
    "/test": {
      "get": {
        "tags": [
          "basic-controller"
        ],
        "summary": "get",
        "description": "Provides a response.",
        "operationId": "get",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/hal+json": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Link": {
        "type": "object",
        "properties": {
          "href": {
            "type": "string"
          },
          "hreflang": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "deprecation": {
            "type": "string"
          },
          "profile": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "templated": {
            "type": "boolean"
          }
        }
      },
      "Links": {
        "type": "object",
        "additionalProperties": {
          "$ref": "#/components/schemas/Link"
        }
      }
    }
  }
}

Additional context

The issue did not occur before v2.8.2, and it is still present in v2.8.4.
I think it may be related to this commit 88f5da0 introducing the HateoasLinkConverter. But it's not clear what should be done to fix it.

Maybe, the swagger-core.ModelResolver should be called with a new AnnotatedType(returnType) .resolveAsRef(true).jsonViewAnnotation(jsonView).ctxAnnotations(annotations) as it is done in SpringDocAnnotationsUtils.extractSchema() line 138 ? Because, here it gets called with resolveAsRef(false), then the ref is not added to the schema at line ModelResolver.resolve() line 1060 and then it breaks in HateoasLinksConverter.resolve() line 74.

Thank you for your help :)

@smurf667
Copy link

Just want to add, we're seeing the same problem.

@bnasslahsen
Copy link
Collaborator

@didjoman,

Not reproducible.
You can review the project.

git clone https://github.com/didjoman/springdoc-nullable_fields-issue
mvn spring-boot:run
curl --location --request GET 'http://localhost:8080/api-docs' --header 'Content-Type: application/json'

This the result of your project:

{
   "openapi":"3.1.0",
   "info":{
      "title":"OpenAPI definition",
      "version":"v0"
   },
   "servers":[
      {
         "url":"http://localhost:8080",
         "description":"Generated server url"
      }
   ],
   "paths":{
      "/cat":{
         "get":{
            "tags":[
               "basic-controller"
            ],
            "summary":"get",
            "description":"Provides an animal.",
            "operationId":"get",
            "parameters":[
               {
                  "name":"cat",
                  "in":"query",
                  "required":true,
                  "schema":{
                     "$ref":"#/components/schemas/Cat"
                  }
               }
            ],
            "responses":{
               "200":{
                  "description":"OK",
                  "content":{
                     "*/*":{
                        "schema":{
                           "type":"string"
                        }
                     }
                  }
               }
            }
         }
      }
   },
   "components":{
      "schemas":{
         "Cat":{
            "type":"object",
            "description":"Represents a Cat class.",
            "properties":{
               "name":{
                  "type":"string",
                  "description":"The name."
               }
            }
         }
      }
   }
}

This ticket will be closed, but can be reopened if your provide the reproducible sample.

@smurf667
Copy link

@bnasslahsen the NullPointerException occurs in org.springdoc.core.converters.HateoasLinksConverter.resolve(AnnotatedType, ModelConverterContext, Iterator<ModelConverter>) as shown in the following screenshot. Maybe the code can be made safer (not talking about behavior, but to avoid having a flow with an exception, and a NPE at that):

Image

@bnasslahsen
Copy link
Collaborator

@smurf667,

Would you be able to provide a reproducible sample?

@smurf667
Copy link

Hi @bnasslahsen yes - try this: https://github.com/smurf667/springdoc-npe

@bnasslahsen bnasslahsen reopened this Feb 16, 2025
@bnasslahsen bnasslahsen added the bug Something isn't working label Feb 16, 2025
@smurf667
Copy link

smurf667 commented Feb 16, 2025

Thank you for the weekend work, much appreciated 👍

@didjoman
Copy link
Author

didjoman commented Feb 17, 2025

Hi,
Sorry for the bad link (it was good at click, but bad in display -_-'), I updated it,
Thank you so much for the quick fix and the week-end work !

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
Development

No branches or pull requests

3 participants