redocly-cli icon indicating copy to clipboard operation
redocly-cli copied to clipboard

unevaluatedItems boolean schema error, Can't resolve $ref with component schemas

Open jeremyfiel opened this issue 1 year ago • 6 comments

Describe the bug

Two issues here:

1. unevaluatedItems is reporting an error Expected type 'Schema' but got 'Boolean' related to #457 and #532, I don't see unevaluatedItems updated similar to additionalProperties where it can be a boolean or a schema.

  1. Can't resolve $ref for Challenge and File component schemas. These two referenced schemas are not resolved but they appear to be correctly defined in items array. I tried using https://json-schema.hyperjump.io and it says my schema is valid.

To Reproduce Steps to reproduce the behavior:

  1. Given this redocly.yaml file
extends:
 - recommended
  1. And this OpenAPI file(s)
OAS 3.1 schema
{
    "openapi": "3.1.0",
    "info": {
        "title": "freeCodeCamp Classroom",
        "version": "0.0.1",
        "contact": {
            "name": "Classroom Team",
            "email": ""
        },
        "license": {
            "name": "",
            "url": ""
        }
    },
    "servers": [
        {
            "url": "https://api.freecodecamp.org"
        }
    ],
    "security": [],
    "tags": [
        {
            "name": "Classroom user data",
            "description": ""
        }
    ],
    "paths": {
        "/classroom/userData": {
            "get": {
                "summary": "",
                "description": "",
                "operationId": "getUserData",
                "tags": [
                    "Classroom user data"
                ],
                "parameters": [
                    {
                        "$ref": "#/components/parameters/header_accept"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/userData"
                                },
                                "examples": {
                                    "Classroom_User_Data": {
                                        "$ref": "#/components/examples/userData"
                                    }
                                }
                            }
                        }
                    },
                    "400": {
                        "$ref": "#/components/responses/400"
                    },
                    "401": {
                        "$ref": "#/components/responses/401"
                    },
                    "403": {
                        "$ref": "#/components/responses/403"
                    },
                    "404": {
                        "$ref": "#/components/responses/404"
                    },
                    "500": {
                        "$ref": "#/components/responses/500"
                    },
                    "503": {
                        "$ref": "#/components/responses/503"
                    }
                }
            },
            "post": {
                "summary": "",
                "description": "",
                "operationId": "queryUserData",
                "tags": [
                    "Classroom user data"
                ],
                "parameters": [
                    {
                        "$ref": "#/components/parameters/header_contentType"
                    }
                ],
                "requestBody": {
                    "$ref": "#/components/requestBodies/userDataRequest"
                },
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/userData"
                                },
                                "examples": {
                                    "Classroom_User_Data_Request": {
                                        "$ref": "#/components/examples/userData"
                                    }
                                }
                            }
                        }
                    },
                    "400": {
                        "$ref": "#/components/responses/400"
                    },
                    "401": {
                        "$ref": "#/components/responses/401"
                    },
                    "403": {
                        "$ref": "#/components/responses/403"
                    },
                    "404": {
                        "$ref": "#/components/responses/404"
                    },
                    "500": {
                        "$ref": "#/components/responses/500"
                    },
                    "503": {
                        "$ref": "#/components/responses/503"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "problem_json_error": {
                "type": "object",
                "properties": {
                    "type": {
                        "type": "string"
                    },
                    "status": {
                        "type": "number",
                        "minimum": 200,
                        "maximum": 511
                    },
                    "title": {
                        "type": "string"
                    },
                    "detail": {
                        "type": "string"
                    },
                    "instance": {
                        "type": "string",
                        "format": "uri-reference"
                    }
                }
            },
            "userData": {
                "$id": "https://api.freecodecamp.org/classroom/userData",
                "$schema": "https://json-schema.org/draft/2020-12/schema",
                "description": "list of students",
                "type": "array",
                "uniqueItems": true,
                "items": {
                    "type": "object",
                    "properties": {
                        "user": {
                            "type": "object",
                            "properties": {
                                "id": {
                                    "type": "string"
                                },
                                "email": {
                                    "description": "student identifier",
                                    "type": "string",
                                    "format": "email"
                                }
                            }
                        },
                        "certifications": {
                            "description": "name of the certification (\"2022/responsive-web-design\")",
                            "type": "array",
                            "uniqueItems": true,
                            "minItems": 1,
                            "items": {
                                "description": "name of the certification block",
                                "type": "object",
                                "propertyNames": {
                                    "pattern": "^[A-Za-z0-9][\/A-Za-z0-9-]*[A-Za-z]$"
                                },
                                "additionalProperties": {
                                    "type": "object",
                                    "unevaluatedProperties": false,
                                    "properties": {
                                        "blocks": {
                                            "description": "List of the challenges/blocks inside of a certification",
                                            "type": "array",
                                            "uniqueItems": true,
                                            "items": {
                                                "type": "object",
                                                "minProperties": 1,
                                                "additionalProperties": {
                                                    "description": "name of the block (learn-css-...)",
                                                    "type": "object",
                                                    "properties": {
                                                        "completedChallenges": {
                                                            "type": "array",
                                                            "description": "list of challenges completed by the student inside of this block",
                                                            "uniqueItems": true,
                                                            "items": {
                                                                "$ref": "#/$defs/Challenge"
                                                            }
                                                        }
                                                    },
                                                    "required": [
                                                        "completedChallenges"
                                                    ]
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "required": [
                        "user",
                        "certifications"
                    ]
                },
                "$defs": {
                    "Challenge": {
                        "description": "A representation of an individual challenge",
                        "type": "object",
                        "unevaluatedProperties": false,
                        "required": [
                            "id",
                            "challengeName",
                            "completedDate"
                        ],
                        "properties": {
                            "id": {
                                "description": "A unique identifier of a completed course",
                                "type": "string",
                                "minLength": 1
                            },
                            "challengeName": {
                                "type": "string"
                            },
                            "completedDate": {
                                "description": "A Unix timestamp representation",
                                "type": "integer"
                            },
                            "files": {
                                "description": "A list of files pertaining to the challenge",
                                "type": "array",
                                "uniqueItems": true,
                                "unevaluatedItems": false,
                                "items": {
                                    "$ref": "#/$defs/File"
                                }
                            }
                        }
                    },
                    "File": {
                        "description": "A representation of a file",
                        "type": "string",
                        "format": "binary"
                    }
                }
            }
        },
        "parameters": {
            "header_accept": {
                "name": "accept",
                "description": "",
                "in": "header",
                "schema": {
                    "type": "string"
                },
                "examples": {
                    "accept_header": {
                        "summary": "A standard header indicating the accepted media type",
                        "value": "application/json"
                    }
                }
            },
            "header_contentType": {
                "name": "content-type",
                "description": "",
                "in": "header",
                "schema": {
                    "type": "string"
                },
                "examples": {
                    "content-type": {
                        "summary": "A standard header indicating the content media type",
                        "value": "application/json"
                    }
                }
            }
        },
        "requestBodies": {
            "userDataRequest": {
                "description": "",
                "content": {
                    "application/json": {
                        "schema": {
                            "$schema": "https://json-schema.org/draft/2020-12/schema",
                            "title": "UserDataRequest",
                            "description": "Queries a collection of users and certifications",
                            "type": "object",
                            "unevaluatedProperties": false,
                            "properties": {
                                "users": {
                                    "type": "array",
                                    "uniqueItems": true,
                                    "minItems": 1,
                                    "prefixItems": [
                                        {
                                            "type": "object",
                                            "unevaluatedProperties": false,
                                            "properties": {
                                                "id": {
                                                    "type": "string"
                                                }
                                            },
                                            "required": [
                                                "id"
                                            ]
                                        }
                                    ],
                                    "items": {
                                        "description": "name of the certification block",
                                        "type": "string",
                                        "pattern": "^[A-Za-z0-9][\/A-Za-z0-9-]*[A-Za-z]$"
                                    }
                                }
                            },
                            "required": [
                                "users"
                            ]
                        },
                        "examples": {
                            "Classroom_User_Data_Request": {
                                "$ref": "#/components/examples/userDataRequest"
                            }
                        }
                    }
                },
                "required": true
            }
        },
        "responses": {
            "400": {
                "description": "Bad Request",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/problem_json_error"
                        },
                        "examples": {
                            "Bad_request_problem_json": {
                                "summary": "A problem+json representation of an error",
                                "$ref": "#/components/examples/Bad_request_problem_json"
                            }
                        }
                    }
                }
            },
            "401": {
                "description": "Unauthorized",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/problem_json_error"
                        }
                    }
                }
            },
            "403": {
                "description": "Forbidden",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/problem_json_error"
                        }
                    }
                }
            },
            "404": {
                "description": "Not Found",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/problem_json_error"
                        }
                    }
                }
            },
            "500": {
                "description": "Internal Server Error",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/problem_json_error"
                        }
                    }
                }
            },
            "503": {
                "description": "Service Unavailable",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/problem_json_error"
                        }
                    }
                }
            }
        },
        "examples": {
            "userData": {
                "summary": "userData from the Classroom",
                "value": [
                    {
                        "user": {
                            "id": "",
                            "email": "[email protected]"
                        },
                        "certifications": [
                            {
                                "2022/responsive-web-design": {
                                    "blocks": [
                                        {
                                            "learn-basic-css-by-building-a-cafe-menu": {
                                                "completedChallenges": [
                                                    {
                                                        "id": "5f33071498eb2472b87ddee4",
                                                        "challengeName": "Step 1",
                                                        "completedDate": 1475094716730,
                                                        "files": []
                                                    },
                                                    {
                                                        "id": "5f3313e74582ad9d063e3a38",
                                                        "challengeName": "Step 2",
                                                        "completedDate": 1537207306322,
                                                        "files": []
                                                    }
                                                ]
                                            }
                                        }
                                    ]
                                }
                            },
                            {
                                "quality-assurance": {
                                    "blocks": [
                                        {
                                            "advanced-node-and-express": {
                                                "completedChallenges": [
                                                    {
                                                        "id": "5895f700f9fc0f352b528e63",
                                                        "challengeName": "Set up a Template Engine",
                                                        "completedDate": 98448684,
                                                        "files": []
                                                    },
                                                    {
                                                        "id": "5895f70df9fc0f352b528e6a",
                                                        "challengeName": "Create New Middleware",
                                                        "completedDate": 98448643284,
                                                        "files": []
                                                    }
                                                ]
                                            }
                                        },
                                        {
                                            "quality-assurance-and-testing-with-chai": {
                                                "completedChallenges": [
                                                    {
                                                        "id": "587d824a367417b2b2512c46",
                                                        "challengeName": "Learn How JavaScript Assertions Work",
                                                        "completedDate": 47664591,
                                                        "files": []
                                                    }
                                                ]
                                            }
                                        }
                                    ]
                                }
                            }
                        ]
                    }
                ]
            },
            "Bad_request_problem_json": {
                "summary": "A problem+json representation of an error",
                "value": {
                    "type": "Bad Request error",
                    "status": 400,
                    "title": "400 - Bad Request",
                    "detail": "You submitted a request payload with invalid JSON. Please validate your instance and resubmit"
                }
            },
            "userDataRequest": {
                "summary": "",
                "value": {
                    "users": [
                        {
                            "id": "123456"
                        },
                        "2022/responsive-web-design",
                        "quality-assurance"
                    ]
                }
            }
        }
    }
}
  1. Run this command with these arguments... redocly lint --config redocly.yml fcc-classroom-userdata-openapi.json --format stylish
  2. See error
fcc-classroom-userdata-openapi.json:
  254:53  error    spec                  Expected type `Schema` (object) but got `boolean`
  255:42  error    no-unresolved-refs    Can't resolve $ref
  206:70  error    no-unresolved-refs    Can't resolve $ref
  228:21  warning  no-unused-components  Component: "Challenge" is never used.
  261:21  warning  no-unused-components  Component: "File" is never used.

Expected behavior

All component schemas should be referenced and unevaluatedItems: false is a valid schema type.

Redocly Version(s)

Redocly OpenAPI - vs code v0.2.24

Node.js Version(s)

v14.18.1

jeremyfiel avatar Aug 15 '23 19:08 jeremyfiel

Thanks for the report @jeremyfiel!

tatomyr avatar Aug 16 '23 09:08 tatomyr

further discussion with JSON Schema folks and they mentioned it may be an issue with resolving the base uri $id in the nested schema. It's possible ajv doesn't support this.

this seems to be a very typical use case where it's equivalent to $ref an external schema defined as 2020-12. In this case, it's dereferenced inside the OAS definition but should resolve in the same manner as if it were externally referenced.

jeremyfiel avatar Aug 17 '23 14:08 jeremyfiel

This is most likely an error with redocly-cli, not ajv. We don't use AJV for $ref resolution. We'll take a look. Thanks!

RomanHotsiy avatar Aug 17 '23 23:08 RomanHotsiy

I just checked it and we indeed do not support $id in the nested schema properly.

RomanHotsiy avatar Oct 05 '23 07:10 RomanHotsiy

Created a PR with potential fix here: https://github.com/Redocly/redocly-cli/pull/1286

@jeremyfiel would you be able to test the fix for your use case?

RomanHotsiy avatar Oct 05 '23 09:10 RomanHotsiy

@tatomyr please reopen for the second item mentioned. @RomanHotsiy was still working on the pr for the embedded $id resolution.

jeremyfiel avatar Oct 27 '23 13:10 jeremyfiel