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

TypeError: Converting circular structure to JSON

Open snakuzzo opened this issue 5 years ago • 20 comments

I'm trying to use a YAML file to create a mock. I'm using example "express-ts-mock" (https://github.com/anttiviljami/openapi-backend/blob/master/examples/express-ts-mock/index.ts)

but this is the result...

snakuzzo@laptop:~/workspace/openapi-backend/examples/express-ts-mock$ npm start

> [email protected] start /home/snakuzzo/workspace/openapi-backend/examples/express-ts-mock
> node dist/index.js

api listening at http://localhost:9000
(node:90915) UnhandledPromiseRejectionWarning: TypeError: Converting circular structure to JSON
    at stringify (/home/snakuzzo/workspace/openapi-backend/examples/express-ts-mock/node_modules/fast-json-stable-stringify/index.js:42:19)
    at stringify (/home/snakuzzo/workspace/openapi-backend/examples/express-ts-mock/node_modules/fast-json-stable-stringify/index.js:50:25)
    at stringify (/home/snakuzzo/workspace/openapi-backend/examples/express-ts-mock/node_modules/fast-json-stable-stringify/index.js:50:25)
    at stringify (/home/snakuzzo/workspace/openapi-backend/examples/express-ts-mock/node_modules/fast-json-stable-stringify/index.js:50:25)
    at stringify (/home/snakuzzo/workspace/openapi-backend/examples/express-ts-mock/node_modules/fast-json-stable-stringify/index.js:50:25)
    at stringify (/home/snakuzzo/workspace/openapi-backend/examples/express-ts-mock/node_modules/fast-json-stable-stringify/index.js:50:25)
    at stringify (/home/snakuzzo/workspace/openapi-backend/examples/express-ts-mock/node_modules/fast-json-stable-stringify/index.js:50:25)
    at stringify (/home/snakuzzo/workspace/openapi-backend/examples/express-ts-mock/node_modules/fast-json-stable-stringify/index.js:50:25)
    at stringify (/home/snakuzzo/workspace/openapi-backend/examples/express-ts-mock/node_modules/fast-json-stable-stringify/index.js:50:25)
    at stringify (/home/snakuzzo/workspace/openapi-backend/examples/express-ts-mock/node_modules/fast-json-stable-stringify/index.js:50:25)
(node:90915) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:90915) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

Any suggestion?

Thanks

snakuzzo avatar Oct 30 '20 15:10 snakuzzo

I'm also having having an issue when using a recursive structure in a parameter. The definitions compile to valid schema, but if I turn off parameter validation or remove the recursive elements of the parameter itself, I don't have an error. @snakuzzo are you dealing with a recursive structure somewhere?

elby22 avatar Oct 30 '20 17:10 elby22

@snakuzzo are you dealing with a recursive structure somewhere?

Yes I need to create a mock and specs are not mine. I cannot edit files, so I cannot remove recursive elements. How can I remove parameter validation ?

snakuzzo avatar Oct 30 '20 17:10 snakuzzo

@snakuzzo you can set {validate: false} in your call to the OpenAPIBackend constructior. Note that this will not validate your requests before they execute your handlers. I've determined the issue comes from validator.buildRequestValidatorsForOperation() wherein the schema is built and passed to ajv to be compiled. We are passing it a de-referenced object form of the schema, not using $refs to handle recursion.

In my case, i'm referring to a #/components/schemas from a #/components/parametersdefinition. In order to use $refs here, we would need to provide AJV access to the definitions we want to reference. My suggestion would to be to dump everything inside of the OpenAPI #/components/ into a new schema inside of 'definitions' and expose this schema to each of the validators. This way we can avoid de-referencing and let Ajv do it. Useful link for de-referencing

elby22 avatar Oct 30 '20 18:10 elby22

I tried to pass JSON schema to ajv and, as you said, it correctly validate schema. {validate: false} Is the only way ? @anttiviljami is there no way to change this behavior?

EDIT: I can't skip validate :( @elby22 do you have an example of your solution ?

snakuzzo avatar Nov 02 '20 14:11 snakuzzo

@snakuzzo if you have circular refs in your parameters (or probably anywhere Ajv is used for validation) and you absolutely must validate those params upon request, then I'm not sure there is a good solution at the moment. I'll be working on a solution later this week and make a PR eventually. In the mean time, you can can probably hack something so before validator.buildRequestValidatorsForOperation() hits validators.push(paramsValidator.compile(paramsSchema)); on line /src/validation.ts#L537, it does not compile a schema with a circular ref. There are probably a bunch of ways to do this which don't require modifying the source. Let me know if you want to collaborate; I believe we have the same problem to solve.

elby22 avatar Nov 03 '20 15:11 elby22

I'd like to collaborate...but I don't know how :) I'm not so experienced

snakuzzo avatar Nov 05 '20 09:11 snakuzzo

Hi @elby22...did you tried something ?!

snakuzzo avatar Nov 12 '20 13:11 snakuzzo

Hey @snakuzzo, I've been working on other stuff lately. Might give it another shot today, but I will definitely get around to it at some point. Would you mind posting a sample of your schema so I can verify we have the same issue?

elby22 avatar Nov 16 '20 14:11 elby22

here it is...

openapi: 3.0.0
info:
  title: 'My example'
  version: '2.2'
servers:
  - url: https://api.example.com/v3
paths:
  '/myexample/{id}':
    get:
      tags:
        - ID 20a
      parameters:
        - name: id
          in: path
          schema:
            type: string
            pattern: '^([A-Z0-9]{16}|[0-9]{11})$'
            example: 'XXXXXXXXXXXXXXX'
          required: true
        - name: fields
          in: query
          schema:
            type: array
            items: 
              type: string
      responses:
        '200':
          description: ok
          headers:
            datoEstratto: 
              schema:
                type: string
          content:
            application/json:
              schema:
                type: array
                items: 
                  $ref: '#/components/schemas/myexample'
        '500':
          description: 'Generic error'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorMessage'
components:
  schemas:
    otherValues:
      type: object
      properties:
        key:
          type: string
          nullable: true
        value:
          type: string
          nullable: true
    itemDetailValues:
      type: object
      properties:
        name:
          type: string
          nullable: true
        itValue:
          type: array
          items:
            $ref: '#/components/schemas/otherValues'
    itemDetail:
      type: object
      properties:
        description:
          type: string
          nullable: true
        itemValues:
          type: array
          items:
            $ref: '#/components/schemas/itemDetailValues'
              
    
    offer:
      type: object
      properties:
        id:
          type: string
          nullable: true
        myservice:
          type: array
          items:
            $ref: '#/components/schemas/myservice'
    
    myservice:
      type: object
      properties:
        id:
          type: string
          nullable: true
          example: "aaaaa"
        item:
          type: array
          items:
            $ref: '#/components/schemas/item'

    item:
      type: object
      properties:
        id:
          type: string
          nullable: true
          example: 'aaaaaaaa'
        itemDetail:
          type: array
          items:
            $ref: '#/components/schemas/itemDetail'
        item:
          type: array
          items:
            $ref: '#/components/schemas/item'

    myexample:
      type: object
      description: 'data'
      required: ["id", "offer"]
      properties:
        id:
          type: string
          pattern: '^([A-Z0-9]{16}|[0-9]{11}):[0-9]+$'
          example: 'XXXXXXXX:XXXXXXXXX'
        offer:
          type: array
          items:
            $ref: '#/components/schemas/offer'

    ErrorMessage:
      type: object
      required: 
        - code
      description: 'error 4xx 5xx'
      properties:
        code:
          type: string  
          description: 'errorcode'

snakuzzo avatar Nov 18 '20 10:11 snakuzzo

@anttiviljami I noticed we use SwaggerParser.dereference instead of SwaggerParser.bundle, which keeps references in-tact for this.definition. Without changing this, I'm not sure this library can support recursive schema definitions while using Ajv. If we keep the references, it would then be possible for a user to implement their own ref-resolving using customizeAjv. Are there other considerations I didn't address here?

elby22 avatar Nov 18 '20 18:11 elby22

Hi guys... any news about this issue ?

snakuzzo avatar Nov 24 '20 08:11 snakuzzo

@snakuzzo working on a PR now - trying to get it out by today.

elby22 avatar Nov 24 '20 14:11 elby22

@snakuzzo I've added a PR which should address this problem without re-writing too much of the library. Until it gets merged however, you may be able to solve the problem by using this in your constructor:

customizeAjv: (ajv, ajvOpts, ValidationContext) => {
	const oldCompile = ajv.compile.bind(ajv);
	ajv.compile = (schema) => {
		const decycledSchema = decycle(schema);
		return oldCompile(decycledSchema);
	}
	return ajv;
}

where decycle is a new function which you can find in the PR.

elby22 avatar Dec 08 '20 19:12 elby22

I will try ASAP. Thank you!!!

snakuzzo avatar Dec 09 '20 01:12 snakuzzo

Hi @elby22. Am I getting this problem right?

  • Ajv can handle circular schemas as long as they're represented with JSON Schema $refs
  • We are calling SwaggerParser.dereference before passing the schemas to Ajv, which causes circular $refs to be converted into circular JavaScript objects, which Ajv cannot handle
  • Your PR adds a new function OpenAPIValidator.decycle, which converts any circular javascript structures back into $refs. This function then processes all schemas before passing them to Ajvs.

Just want to double check to make sure I'm understanding the problem correctly

anttiviljami avatar Jan 10 '21 11:01 anttiviljami

And apologies to both of you @elby22 @snakuzzo for the delay with getting back to you. I've recently joined a new company, which (I hope) understandably is stretching my available time for OSS maintenance work 😔

anttiviljami avatar Jan 10 '21 11:01 anttiviljami

@anttiviljami no problems with the delays - congrats on your new venture! You are correct with my solution to the problem. Keeping the document dereferenced was necessary to handle our programatic use of the document. I had earlier attempts to just maintain $refs but there were too many edge cases where I actually wanted to dereference them.

elby22 avatar Jan 11 '21 14:01 elby22

@elby22 @anttiviljami Was this fix deployed? Just tested the above example and getting a range error when mocking?

RangeError: Maximum call stack size exceeded at Date.valueOf () at Date.[Symbol.toPrimitive] () at Date.toJSON ()

pmparland avatar Jul 14 '21 18:07 pmparland

Adding the decycle function to the mock operation resolved this

pmparland avatar Jul 15 '21 09:07 pmparland

@anttiviljami I think we can close this bug due to the merged PR from above

elby22 avatar Sep 23 '21 02:09 elby22