Skip to content

StackOverflow when response has a circular dependency #21

@joklek

Description

@joklek

When a response has a circular dependency in its definition, converter fails with a StackOverflowError. Stacktrace looks like this:

java.lang.StackOverflowError
	at com.fasterxml.jackson.core.sym.CharsToNameCanonicalizer$TableInfo.createInitial(CharsToNameCanonicalizer.java:809)
	at com.fasterxml.jackson.core.sym.CharsToNameCanonicalizer.<init>(CharsToNameCanonicalizer.java:243)
	at com.fasterxml.jackson.core.sym.CharsToNameCanonicalizer.createRoot(CharsToNameCanonicalizer.java:300)
	at com.fasterxml.jackson.core.sym.CharsToNameCanonicalizer.createRoot(CharsToNameCanonicalizer.java:296)
	at com.fasterxml.jackson.core.JsonFactory.<init>(JsonFactory.java:191)
	at com.fasterxml.jackson.databind.MappingJsonFactory.<init>(MappingJsonFactory.java:29)
	at com.fasterxml.jackson.databind.ObjectMapper.<init>(ObjectMapper.java:549)
	at com.fasterxml.jackson.databind.ObjectMapper.<init>(ObjectMapper.java:480)
// This is the loop start
at blog.svenbayer.springframework.cloud.contract.verifier.spec.swagger.builder.reference.SwaggerDefinitionsRefResolverSwagger.<init>(SwaggerDefinitionsRefResolverSwagger.java:25)
	at blog.svenbayer.springframework.cloud.contract.verifier.spec.swagger.builder.reference.ReferenceResolverFactory.getReferenceResolver(ReferenceResolverFactory.java:32)
	at blog.svenbayer.springframework.cloud.contract.verifier.spec.swagger.builder.ResponseHeaderValueBuilder.createResponseHeaderValue(ResponseHeaderValueBuilder.java:66)
	at blog.svenbayer.springframework.cloud.contract.verifier.spec.swagger.builder.ResponseHeaderValueBuilder.createResponseHeaderValue(ResponseHeaderValueBuilder.java:74)
	at blog.svenbayer.springframework.cloud.contract.verifier.spec.swagger.builder.reference.SwaggerDefinitionsRefResolverSwagger.lambda$resolveObjectDefinitionsRef$0(SwaggerDefinitionsRefResolverSwagger.java:110)
	at java.base/java.util.stream.Collectors.lambda$uniqKeysMapAccumulator$1(Collectors.java:178)
	at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
	at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:578)
// This is the loop end

Here is the test and a json to reproduce this issue:

circular_dependency_swagger.yml

swagger: '2.0'
info:
  title: COFFEE-ROCKET-SERVICE
  description: A service that provides coffee bean rockets, bean planets, and other things the coffeeverse has to offer.
  version: '1.0'
  termsOfService: 'urn:tos'
  contact: {}
  license:
    name: Apache 2.0
    url: 'http://www.apache.org/licenses/LICENSE-2.0'
host: svenbayer.blog
schemes:
  - https
  - http
basePath: /coffee-rocket-service/v1.0
paths:
  /takeoff:
    post:
      x-ignore: false
      summary: Sends a coffee rocket to a bean planet and returns the bean planet.
      description: API endpoint to send a coffee rocket to a bean planet and returns the bean planet.
      consumes:
        - application/json
      produces:
        - '*/*'
      responses:
        '201':
          description: Created
          schema:
            $ref: '#/definitions/BeanPlanet'
definitions:
  BeanPlanet:
    type: object
    properties:
      neighbor_planets:
        type: array
        items:
          $ref: '#/definitions/BeanPlanet'
    title: BeanPlanet

CircularDependencySwaggerContractSpec

class CircularDependencySwaggerContractSpec extends Specification {

    @Subject
    SwaggerContractConverter converter = new SwaggerContractConverter()
    TestContractEquals testContractEquals = new TestContractEquals()

    def "should convert swagger with circular dependency to contract"() {
        given:
        File singleSwaggerYaml = new File(SwaggerContractConverterSpec.getResource("/swagger/circular_dependency/circular_dependency_swagger.yml").toURI())
        Contract expectedContract = Contract.make {
            label("takeoff_coffee_bean_rocket")
            name("1_takeoff_POST")
            description("API endpoint to send a coffee rocket to a bean planet and returns the bean planet.")
            priority(1)
            request {
                method(POST())
                urlPath("/coffee-rocket-service/v1.0/takeoff")
            }
            response {
                status(201)
                body("""{
  "neighbor_planets": [
    null
  ]
}""")
            }
        }
        when:
        Collection<Contract> contracts = converter.convertFrom(singleSwaggerYaml)
        then:
        testContractEquals.assertContractEquals(Collections.singleton(expectedContract), contracts)
    }
}

Swagger UI solves this issue by setting the repeated item to null. Maybe it would be useful to let just one level to be generate, to show the contract more clearly, but any solution would work.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions