Redocly / redoc

📘 OpenAPI/Swagger-generated API Reference Documentation
https://redocly.github.io/redoc/
MIT License
23.58k stars 2.3k forks source link

Error: Incompatible types in allOf at "undefined" #907

Closed pintux closed 4 years ago

pintux commented 5 years ago

Using 2.0.0-rc.4 with an OpenAPI spec 3.0.2 file, parsing fails and I'm getting the following error in the browser console:

redoc.standalone.js:26397 Error: Incompatible types in allOf at "undefined" at e.mergeAllOf (redoc.standalone.js:38564) at redoc.standalone.js:38614 at Array.map () at r (redoc.standalone.js:38613) at e.hoistOneOfs (redoc.standalone.js:38625) at e.mergeAllOf (redoc.standalone.js:38537) at e.mergeAllOf (redoc.standalone.js:38568) at redoc.standalone.js:38861 at Array.map () at e.initOneOf (redoc.standalone.js:38859)

I'm not able to understand the point in which fails.

My OpenAPI spec 3.0.2 json file validates correctly using a tool like oas-validate from https://github.com/Mermade/oas-kit

RomanHotsiy commented 5 years ago

Could you share your spec or at least minimal reproducible sample?

Looks like you are allOfing schemas with different type somewhere in the spec, e.g.:

allOf:
  - type: string
  - type: array
    items:
      type: string
pintux commented 5 years ago

Hi @RomanGotsiy,

you can reproduce the issue with the following complete spec:

{
  "openapi": "3.0.2",
  "info": { "title": "Server REST API v3", "version": "7.0.0-dev1" },
  "components": {
    "schemas": {
      "errorResponse": {
        "type": "object",
        "properties": { "error": { "type": "integer", "minimum": 100 }, "message": { "type": "string" }, "info": { "type": "string" } },
        "required": ["error", "message"]
      },
      "data_collection": {
        "description": "Data Collection",
        "type": "object",
        "allOf": [{ "$ref": "#/components/schemas/hasAccountId" }],
        "properties": {
          "fields": {
            "type": "array",
            "items": {
              "anyOf": [
                { "$ref": "#/components/schemas/metaField" },
                { "$ref": "#/components/schemas/stringField" },
                { "$ref": "#/components/schemas/selectField" },
                { "$ref": "#/components/schemas/numberField" },
                { "$ref": "#/components/schemas/ratingField" },
                { "$ref": "#/components/schemas/booleanField" }
              ]
            }
          }
        }
      },
      "hasAccountId": {
        "type": "object",
        "required": ["acct_id"],
        "properties": { "acct_id": { "description": "Account ID", "type": "string", "readOnly": true } }
      },
      "nonEmptyString": { "type": "string", "minLength": 1 },
      "abstractField": {
        "type": "object",
        "required": ["type"],
        "properties": {
          "id": { "$ref": "#/components/schemas/nonEmptyString" },
          "type": { "$ref": "#/components/schemas/nonEmptyString" },
          "labelId": { "$ref": "#/components/schemas/nonEmptyString" },
          "format": { "$ref": "#/components/schemas/nonEmptyString" }
        }
      },
      "metaField": {
        "type": "object",
        "allOf": [{ "$ref": "#/components/schemas/abstractField" }],
        "oneOf": [
          { "type": "object", "properties": { "format": { "enum": ["break"] } } },
          {
            "type": "object",
            "required": ["id"],
            "properties": { "format": { "enum": ["message"] }, "message": { "$ref": "#/components/schemas/nonEmptyString" } }
          },
          { "type": "object", "required": ["id", "labelId"], "properties": { "format": { "enum": ["section"] }, "implicit": { "type": "boolean" } } }
        ],
        "properties": { "type": { "enum": ["meta"] } }
      },

      "dataField": {
        "type": "object",
        "allOf": [{ "$ref": "#/components/schemas/abstractField" }],
        "properties": {
          "placeholderId": { "type": "string", "description": "label id for a placeholder text to display to the user when no value is provided" },
          "required": { "type": "boolean", "description": "For the Data Collection to be valid, a value must be provided" },
          "hidden": { "type": "boolean", "description": "If true, the field is not displayed to the agent", "default": false },
          "editable": { "type": "boolean", "description": "If true, the agent can modify the value of the field" },
          "defaultConstant": {
            "oneOf": [{ "type": "string" }, { "type": "number" }, { "type": "boolean" }],
            "description": "A default value, maybe overridden by a variable or the user"
          },
          "defaultVariableId": {
            "type": "string",
            "description": "Id of the variable to use to fill the field. If no value is retrieved, defaultConstant is used instead"
          },
          "editIfDefault": {
            "type": "boolean",
            "description": "If true, the field is displayed and can be edited by the user even if a value was set by either a variable or a constant",
            "default": false
          }
        }
      },
      "stringField": {
        "type": "object",
        "required": ["type"],
        "allOf": [{ "$ref": "#/components/schemas/dataField" }],
        "properties": {
          "type": { "type": "string", "enum": ["string"] },
          "format": {
            "type": "string",
            "enum": [
              "text",
              "textarea",
              "email",
              "nickname",
              "firstname",
              "lastname",
              "phonenum",
              "link",
              "date",
              "time",
              "date-time",
              "dropdown",
              "userid",
              "avatar",
              "intprefix",
              "street",
              "city",
              "province",
              "region",
              "state",
              "country"
            ],
            "default": "text"
          },
          "defaultConstant": { "type": "string", "description": "A default value, maybe overridden by a variable or the user" },
          "minLength": { "type": "integer" },
          "maxLength": { "type": "integer" },
          "validation": { "type": "string", "description": "Regular expression to validate the field, not applied to default values" }
        }
      },
      "selectField": {
        "type": "object",
        "required": ["type"],
        "allOf": [{ "$ref": "#/components/schemas/dataField" }],
        "properties": {
          "type": { "enum": ["dropdown"] },
          "options": { "type": "object", "additionalProperties": { "type": "string", "description": "Label id of the option" }, "minProperties": 1 }
        }
      },
      "numberField": {
        "type": "object",
        "required": ["type"],
        "allOf": [{ "$ref": "#/components/schemas/dataField" }],
        "properties": {
          "type": { "type": "string", "enum": ["number"] },
          "format": { "type": "string", "enum": ["number", "rating"], "default": "number" },
          "defaultConstant": { "type": "number", "description": "A default value, maybe overridden by a variable or the user" },
          "min": { "type": "integer" },
          "max": { "type": "integer" }
        }
      },
      "ratingField": {
        "type": "object",
        "required": ["format", "style"],
        "allOf": [{ "$ref": "#/components/schemas/numberField" }],
        "properties": { "format": { "enum": ["rating"] }, "style": { "type": "string" } }
      },
      "booleanField": {
        "type": "object",
        "required": ["type"],
        "allOf": [{ "$ref": "#/components/schemas/dataField" }],
        "properties": {
          "type": { "type": "string", "enum": ["boolean"] },
          "format": { "type": "string", "enum": ["checkbox", "radio"], "default": "checkbox" },
          "defaultConstant": { "type": "boolean", "description": "A default value, maybe overridden by a variable or the user" },
          "trueLabel": { "type": "string", "description": "Id of the string describing the \"true\" value" },
          "falseLabel": { "type": "string", "description": "Id of the string describing the \"false\" value" },
          "validation": { "type": "boolean", "description": "Value that the field must have for the Data Collection to be valid" }
        }
      }
    },
    "responses": {
      "defaultError": {
        "description": "Default/generic error response",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/errorResponse" } } }
      },
      "notFound": {
        "description": "The requested/specified resource was not found",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/errorResponse" } } }
      }
    },
    "parameters": {
      "id": { "description": "Unique identifier of the resource", "name": "id", "in": "path", "schema": { "type": "string" }, "required": true },
      "limit": {
        "name": "limit",
        "in": "query",
        "description": "Maximum number of items to return",
        "schema": { "type": "integer", "default": 20, "minimum": 1, "maximum": 100 }
      },
      "skip": {
        "name": "skip",
        "in": "query",
        "description": "Skip the specified number of items",
        "schema": { "type": "integer", "default": 0, "minimum": 0 }
      },
      "fields": {
        "name": "fields",
        "in": "query",
        "description": "Return only the specified properties",
        "schema": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
      },
      "sort": {
        "name": "sort",
        "in": "query",
        "description": "Sorting criteria, using RQL syntax (a,-b,+c)",
        "schema": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
      },
      "query": {
        "name": "q",
        "in": "query",
        "description": "Return only items matching the specified [RQL](https://github.com/persvr/rql) query. This parameter can also be used to specify the ordering criteria of the results",
        "schema": { "type": "string" }
      },
      "version": { "description": "Resource version", "name": "version", "in": "path", "schema": { "type": "integer" }, "required": true }
    },
    "securitySchemes": {
      "basic": { "type": "http", "scheme": "basic" }
    }
  },
  "paths": {
    "/data-collections": {
      "get": {
        "operationId": "DataCollection.query",
        "tags": ["DataCollection"],
        "responses": {
          "200": {
            "description": "List of matching DataCollections",
            "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/data_collection" } } } },
            "headers": {
              "Link": { "description": "Data pagination links, as described in RFC5988. Currently only rel=next is supported", "schema": { "type": "string" } },
              "Results-Matching": { "description": "Total number of resources matching the query", "schema": { "type": "integer", "minimum": 0 } },
              "Results-Skipped": {
                "description": "Number of resources skipped to return the current batch of resources",
                "schema": { "type": "integer", "minimum": 0 }
              }
            }
          },
          "default": { "$ref": "#/components/responses/defaultError" }
        },
        "summary": "Retrieve a list of DataCollections",
        "parameters": [
          { "$ref": "#/components/parameters/limit" },
          { "$ref": "#/components/parameters/skip" },
          { "$ref": "#/components/parameters/fields" },
          { "$ref": "#/components/parameters/sort" },
          { "$ref": "#/components/parameters/query" }
        ],
        "security": [{ "basic": [] }]
      },
      "post": {
        "operationId": "DataCollection.create",
        "tags": ["DataCollection"],
        "responses": {
          "201": {
            "description": "DataCollection successfully created",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/data_collection" } } },
            "headers": { "Location": { "description": "URI of the newly created resource", "schema": { "type": "string", "format": "uri" } } }
          },
          "default": { "$ref": "#/components/responses/defaultError" }
        },
        "summary": "Create a new DataCollection",
        "requestBody": {
          "description": "DataCollection to be created, omitting  the metadata",
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/data_collection" } } },
          "required": true
        },
        "security": [{ "basic": [] }]
      }
    }
  },
  "tags": [{ "name": "DataCollection", "description": "Data Collection Resource", "x-id": "id", "x-summary-fields": ["id", "labelId"] }],
  "servers": [{ "url": "https://my-foo-server.test.com/api/v3" }]
}
RomanHotsiy commented 5 years ago

This looks like an issue in ReDoc with allOf merging.

You have a pretty complex schema. I think I've fixed it locally but the fix is a bit dangerous so I wanna test it carefully.

Hopefully will land in the upcoming release

sujaybhowmick commented 5 years ago

I am facing the same issue, is there a way I can get the local fix you made? so that I can test it on my local branch?

RomanHotsiy commented 5 years ago

@sujaybhowmick, Would be simpler if you just share your spec or minimal reproducible sample.

sujaybhowmick commented 5 years ago
  "openapi": "3.0.0",
  "info": {
    "version": "1.0.0",
    "title": "ReDoc Bug",
    "description": "ReDoc Bug\n"
  },
  "servers": [
    {
      "url": "some server",
      "description": "some description"
    }
  ],
  "tags": [
    {
      "name": "MyAPI",
      "description": "Some API"
    }
  ],
  "paths": {
    "/myapi": {
      "post": {
        "tags": [
          "MyAPI"
        ],
        "summary": "",
        "description": "some description\n",
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "description": "Authorization Header Base64 encoded String",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "pramam1": {
                    "type": "string",
                    "enum": [
                      "password"
                    ],
                    "description": "Grant type supported by OAuth implementation\n"
                  },
                  "param2": {
                    "type": "string",
                    "description": "some param 2\n"
                  },
                  "param3": {
                    "type": "string",
                    "description": "some description\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "type": "string",
                      "properties": {
                        "name": {
                          "type": "string"
                        }
                      }
                    },
                    {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    }
  }
}

Error produce when trying to use redoc-cli

Prerendering docs
Error: Incompatible types in allOf at ""
    at OpenAPIParser.mergeAllOf (/Users/sujaybhowmick/.nvm/versions/node/v12.2.0/lib/node_modules/redoc-cli/node_modules/redoc/bundles/redoc.lib.js:8385:23)
    at new SchemaModel (/Users/sujaybhowmick/.nvm/versions/node/v12.2.0/lib/node_modules/redoc-cli/node_modules/redoc/bundles/redoc.lib.js:8610:30)
    at new MediaTypeModel (/Users/sujaybhowmick/.nvm/versions/node/v12.2.0/lib/node_modules/redoc-cli/node_modules/redoc/bundles/redoc.lib.js:8875:38)
    at /Users/sujaybhowmick/.nvm/versions/node/v12.2.0/lib/node_modules/redoc-cli/node_modules/redoc/bundles/redoc.lib.js:8945:20
    at Array.map (<anonymous>)
    at new MediaContentModel (/Users/sujaybhowmick/.nvm/versions/node/v12.2.0/lib/node_modules/redoc-cli/node_modules/redoc/bundles/redoc.lib.js:8942:45)
    at new ResponseModel (/Users/sujaybhowmick/.nvm/versions/node/v12.2.0/lib/node_modules/redoc-cli/node_modules/redoc/bundles/redoc.lib.js:9012:28)
    at /Users/sujaybhowmick/.nvm/versions/node/v12.2.0/lib/node_modules/redoc-cli/node_modules/redoc/bundles/redoc.lib.js:9141:24
    at Array.map (<anonymous>)
    at OperationModel.get (/Users/sujaybhowmick/.nvm/versions/node/v12.2.0/lib/node_modules/redoc-cli/node_modules/redoc/bundles/redoc.lib.js:9140:18)
RomanHotsiy commented 5 years ago

@sujaybhowmick You have the issue in your spec:

"allOf": [
  {
    "type": "string",
    "properties": {
      "name": {
        "type": "string"
      }
    }
  },
  {
    "type": "array",
    "items": {
      "type": "string"
    }
  }
]

While this JSON schema is technically valid it is incorrect: there is no valid data that validates against this schema.

allOf means that data must validate against all of the subschemas.

In your case, if the data is string it can't be array at the same time and vice versa so your schema won't validate any data. You can try it in JSON schema validator: https://www.jsonschemavalidator.net/

Also, the usage of type: "string" and properties in the first allOf item doesn't make any sense too. This part will validate only against string and properties doesn't matter here.

I would recommend you learning JSON schema more deeply, here is the best resource I've seen.

sujaybhowmick commented 5 years ago

How about changing it to

openapi: "3.0.0"
info:
  version: 1.0.0
  title: ReDoc Bug
  description: |
    ReDoc Bug
servers:
  - url: some server
    description: some description
tags:
  - name: MyAPI
    description: Some API
paths:
  '/myapi':
    post:
      tags:
        - MyAPI
      summary: >-

      description: |
        some description
      parameters:
        - name: Authorization
          in: header
          description: Authorization Header Base64 encoded String
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                pramam1:
                  type: string
                  enum: ['password']
                  description: |
                    Grant type supported by OAuth implementation
                param2:
                  type: string
                  description: |
                    some param 2
                param3:
                  type: string
                  description: |
                    some description
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      name:
                        type: string
                  - type: array
                    items:
                      type: string
sujaybhowmick commented 5 years ago

This works, looks like ReDoc cannot handle mixed allOf types. I was trying to keep the example simple.

openapi: "3.0.0"
info:
  version: 1.0.0
  title: ReDoc Bug
  description: |
    ReDoc Bug
servers:
  - url: some server
    description: some description
tags:
  - name: MyAPI
    description: Some API
paths:
  '/myapi':
    post:
      tags:
        - MyAPI
      summary: >-

      description: |
        some description
      parameters:
        - name: Authorization
          in: header
          description: Authorization Header Base64 encoded String
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                pramam1:
                  type: string
                  enum: ['password']
                  description: |
                    Grant type supported by OAuth implementation
                param2:
                  type: string
                  description: |
                    some param 2
                param3:
                  type: string
                  description: |
                    some description
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      name:
                        type: string
                  - type: object
                    properties:
                      id:
                        type: string
sujaybhowmick commented 5 years ago

Sorry I am using YAML language, the JSON equivalents for the problematic part

Working example

"responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        }
                      }
                    },
                    {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }

Bug example

{
  "openapi": "3.0.0",
  "info": {
    "version": "1.0.0",
    "title": "ReDoc Bug",
    "description": "ReDoc Bug\n"
  },
  "servers": [
    {
      "url": "some server",
      "description": "some description"
    }
  ],
  "tags": [
    {
      "name": "MyAPI",
      "description": "Some API"
    }
  ],
  "paths": {
    "/myapi": {
      "post": {
        "tags": [
          "MyAPI"
        ],
        "summary": "",
        "description": "some description\n",
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "description": "Authorization Header Base64 encoded String",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "pramam1": {
                    "type": "string",
                    "enum": [
                      "password"
                    ],
                    "description": "Grant type supported by OAuth implementation\n"
                  },
                  "param2": {
                    "type": "string",
                    "description": "some param 2\n"
                  },
                  "param3": {
                    "type": "string",
                    "description": "some description\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        }
                      }
                    },
                    {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    }
  }
}
sujaybhowmick commented 5 years ago

Thank you for building this and sharing the code, I will try to fix it myself if possible

thiago670 commented 5 years ago

Same problem here friends, I'll try evaluate that suggestions...In the SwaggerHub mydocs works perfectly.

sujaybhowmick commented 5 years ago

Yeah, I switched to swaggerhub and it is really worth the money very professional and has awesome integration and response on issues. Our customers who are very big banks also love it

thiago670 commented 5 years ago

Now working my redoc docs and I've understood the point here.

In the swaggerhub you can "override" properties, because of that my docs was working there. In the opposite, Redoc doesn't allow that.

So, You can't have same properties in allof schemas with different types.

@sujaybhowmick Redoc is a good free tool, but if the clients agrees to pay for it, go ahead!

Best Regards!

sujaybhowmick commented 5 years ago

@thiago670, Of course, it is a good tool and is free. But if I am charging my client, I need to make sure I am able to service them for what they pay.

Bahus commented 4 years ago

Any progress on this? Type override is still not supported.

Gianlo98 commented 4 years ago

Same problem!

dcarbone commented 4 years ago

While I generally laud strictness when defining API's, I'm running into a particular issue where I'm building a middleware in front of a vendor appliance. Most of the API's are passthrough, where my middleware is merely performing what is needed to auth the incoming request before sending it on to the upstream appliance.

As such, I selectively expose the raw swagger annotations for the endpoints that I filter that the upstream appliance itself provides. The issue is these docs are messy, but there are around 760 paths and 950+ definitions. To attempt to clean up the "types" on each of these models would be possible as I am generating these docs programmatically, but it would be really nice if there was maybe a flag that would enable something similar to this logic:

if L.Type && R.Type
-- if config.Strict
---- compare types, fail if incompatible
-- else
---- return R.Type
else if ! L.Type && R.Type
-- return R.Type
else if L.Type && ! R.Type
-- return L.Type

This absolutely does not solve all cases and will assuredly produce issues of its own, however I presume that most of the time (as is my requirement), you'll want to use the right-hand-most value of type value conflicts when merging in an allOf block.

dcarbone commented 4 years ago

For me this was addressed by 6e607b9a2928b062c7705087432c0f0d88e74f5d.