apigee-127 / swagger-tools

A Node.js and browser module that provides tooling around Swagger.
MIT License
702 stars 371 forks source link

Swagger with extensions fails to validate #597

Open alasdairhurst opened 5 years ago

alasdairhurst commented 5 years ago

Given the following swagger file (valid), swagger tools fails to validate it. Error: cannot read property properties of null

It is assuming that all keys under "paths" are paths and not checking for extensions which may be there. There may be other issues too.

{
    "swagger": "2.0",
    "x-an-extension-null": null,
    "x-an-extension-primitive": 1,
    "x-an-extension-array": [],
    "x-an-extension-object": {},
    "host": "localhost:8080",
    "basePath": "/test",
    "schemes": [
        "http",
        "https",
        "ws",
        "wss"
    ],
    "consumes": [
        "application/json"
    ],
    "produces": [
        "application/json"
    ],
    "info": {
        "version": "0.0.0",
        "title": "Thing API",
        "contact": {
            "name": "Person",
            "url": "http://www.person.com",
            "email": "foo@bar.com",
            "x-an-extension-null": null,
            "x-an-extension-primitive": 1,
            "x-an-extension-array": [],
            "x-an-extension-object": {}
        },
        "license": {
            "name": "FREE",
            "url": "http://www.person.com",
            "x-an-extension-null": null,
            "x-an-extension-primitive": 1,
            "x-an-extension-array": [],
            "x-an-extension-object": {}
        },
        "x-an-extension-null": null,
        "x-an-extension-primitive": 1,
        "x-an-extension-array": [],
        "x-an-extension-object": {}
    },
    "parameters": {
        "apiKey": {
            "in": "query",
            "name": "apiKey",
            "type": "string",
            "required": true
        },
        "name": {
            "name": "name",
            "enum": [
                "foo",
                "bar",
                "baz"
            ],
            "in": "query",
            "type": "string",
            "format": "password",
            "allowEmptyValue": true
        },
        "fooArr": {
            "name": "fooArr",
            "in": "query",
            "type": "array",
            "description": "an array of array of strings no bigger than 10 characters beginning with h y or m",
            "collectionFormat": "pipes",
            "uniqueItems": true,
            "default": [
                [
                    "hello",
                    "you"
                ],
                [
                    "me"
                ]
            ],
            "items": {
                "type": "array",
                "collectionFormat": "csv",
                "default": [
                    "hello",
                    "me"
                ],
                "items": {
                    "type": "string",
                    "maxLength": 10,
                    "minLength": 2,
                    "pattern": "^[hmy]"
                }
            }
        },
        "barInt": {
            "x-an-extension-null": null,
            "x-an-extension-primitive": 1,
            "x-an-extension-array": [],
            "x-an-extension-object": {},
            "in": "query",
            "description": "an integer which is a multiple of 5 but bigger than 5 and no bigger than 100",
            "name": "barInt",
            "type": "integer",
            "multipleOf": 5,
            "maximum": 100,
            "exclusiveMaximum": false,
            "minimum": 5,
            "exclusiveMinimum": true
        }
    },
    "paths": {
        "x-an-extension-null": null,
        "x-an-extension-primitive": 1,
        "x-an-extension-array": [],
        "x-an-extension-object": {
            "get": {
                "description": "this is an x- extension and not a path/method",
                "responses": {
                    "default": {
                        "description": "too lazy to make a 200"
                    }
                }
            }
        },
        "x-not-a-path-but-used-in-ref": {
            "get": {
                "description": "this is a method used by a path which isn't 'x-not-a-path-but-used-in-ref'",
                "responses": {
                    "default": {
                        "description": "too lazy to make a 200"
                    }
                }
            }
        },
        "/thingEmpty": {},
        "/thingyref": {
            "$ref": "#/paths/x-not-a-path-but-used-in-ref"
        },
        "/thing": {
            "parameters": [
                {
                    "$ref": "#/parameters/apiKey"
                }
            ],
            "get": {
                "x-an-extension-null": null,
                "x-an-extension-primitive": 1,
                "x-an-extension-array": [],
                "x-an-extension-object": {},
                "description": "has no operation id, no parameters",
                "responses": {
                    "default": {
                        "description": "too lazy to make a 200"
                    }
                }
            },
            "patch": {
                "x-an-extension-null": null,
                "x-an-extension-primitive": 1,
                "x-an-extension-array": [],
                "x-an-extension-object": {},
                "description": "updates name",
                "parameters": [
                    {
                        "name": "name",
                        "in": "query",
                        "type": "string"
                    },
                    {
                        "name": "apiKey",
                        "in": "query",
                        "type": "number",
                        "description": "Overrides the path item parameters"
                    }
                ],
                "responses": {
                    "204": {
                        "description": "done"
                    }
                }
            },
            "delete": {
                "x-an-extension-null": null,
                "x-an-extension-primitive": 1,
                "x-an-extension-array": [],
                "x-an-extension-object": {},
                "tags": [
                    "foo",
                    "bar",
                    "baz"
                ],
                "summary": "interesting endpoint which deletes something for the sake of it.",
                "externalDocs": {
                    "description": "foo",
                    "url": "http://www.person.com",
                    "x-an-extension-null": null,
                    "x-an-extension-primitive": 1,
                    "x-an-extension-array": [],
                    "x-an-extension-object": {}
                },
                "operationId": "deleteNameUnique",
                "consumes": [
                    "application/custom+json"
                ],
                "produces": [
                    "application/custom+json"
                ],
                "description": "deletes name",
                "parameters": [
                    {
                        "$ref": "#/parameters/name"
                    },
                    {
                        "$ref": "#/parameters/fooArr"
                    },
                    {
                        "$ref": "#/parameters/barInt"
                    }
                ],
                "responses": {
                    "201": {
                        "description": "done"
                    }
                }
            },
            "put": {
                "x-an-extension-null": null,
                "x-an-extension-primitive": 1,
                "x-an-extension-array": [],
                "x-an-extension-object": {},
                "operationId": "uploadThing",
                "consumes": [
                    "multipart/form-data"
                ],
                "parameters": [
                    {
                        "x-an-extension-null": null,
                        "x-an-extension-primitive": 1,
                        "x-an-extension-array": [],
                        "x-an-extension-object": {},
                        "name": "thing",
                        "in": "formData",
                        "description": "file to upload",
                        "required": true,
                        "type": "file"
                    }
                ],
                "responses": {
                    "default": {
                        "description": "Updated"
                    }
                }
            },
            "post": {
                "x-an-extension-null": null,
                "x-an-extension-primitive": 1,
                "x-an-extension-array": [],
                "x-an-extension-object": {},
                "operationId": "upsert",
                "parameters": [
                    {
                        "name": "thing",
                        "in": "body",
                        "schema": {
                            "$ref": "#/definitions/wannaBeThing"
                        }
                    }
                ],
                "schemes": [
                    "http",
                    "https"
                ],
                "deprecated": true,
                "responses": {
                    "200": {
                        "x-an-extension-null": null,
                        "x-an-extension-primitive": 1,
                        "x-an-extension-array": [],
                        "x-an-extension-object": {},
                        "description": "Updated",
                        "schema": {
                            "$ref": "#/definitions/thing"
                        },
                        "headers": {
                            "Location": {
                                "x-an-extension-null": null,
                                "x-an-extension-primitive": 1,
                                "x-an-extension-array": [],
                                "x-an-extension-object": {},
                                "type": "string",
                                "description": "Location of new resource"
                            }
                        },
                        "examples": {
                            "application/json": {
                                "name": "Puma",
                                "id": "Dog"
                            }
                        }
                    },
                    "201": {
                        "description": "Created",
                        "headers": {
                            "Location": {
                                "type": "string",
                                "description": "Location of new resource"
                            }
                        }
                    },
                    "204": {
                        "description": "Updated no content"
                    },
                    "400": {
                        "description": "Bad request",
                        "schema": {
                            "$ref": "#/definitions/error"
                        }
                    },
                    "500": {
                        "description": "Server error",
                        "schema": {
                            "$ref": "#/definitions/serverError"
                        }
                    },
                    "501": {
                        "description": "Inline schema",
                        "schema": {
                            "type": "object",
                            "description": "why not"
                        }
                    },
                    "x-an-extension-null": null,
                    "x-an-extension-primitive": 1,
                    "x-an-extension-array": [],
                    "x-an-extension-object": {},
                    "default": {
                        "$ref": "#/responses/serverError"
                    }
                }
            }
        }
    },
    "responses": {
        "serverError": {
            "description": "Server error",
            "schema": {
                "$ref": "#/definitions/serverError"
            }
        }
    },
    "definitions": {
        "geo": {
            "description": "A geographical coordinate",
            "type": "object",
            "properties": {
                "latitude": {
                    "type": "number"
                },
                "longitude": {
                    "type": "number"
                }
            }
        },
        "wannaBeThing": {
            "x-an-extension-null": null,
            "x-an-extension-primitive": 1,
            "x-an-extension-array": [],
            "x-an-extension-object": {},
            "discriminator": "name",
            "type": "object",
            "properties": {
                "name": {
                    "type": "string"
                }
            },
            "required": [
                "name"
            ]
        },
        "thing": {
            "x-an-extension-null": null,
            "x-an-extension-primitive": 1,
            "x-an-extension-array": [],
            "x-an-extension-object": {},
            "type": "object",
            "properties": {
                "id": {
                    "type": "number"
                },
                "name": {
                    "type": "string"
                }
            },
            "required": [
                "id",
                "name"
            ]
        },
        "serverError": {
            "x-an-extension-null": null,
            "x-an-extension-primitive": 1,
            "x-an-extension-array": [],
            "x-an-extension-object": {},
            "type": "object",
            "properties": {
                "message": {
                    "type": "string"
                },
                "location": {
                    "$ref": "#/definitions/geo"
                }
            },
            "required": [
                "message"
            ]
        },
        "error": {
            "x-an-extension-null": null,
            "x-an-extension-primitive": 1,
            "x-an-extension-array": [],
            "x-an-extension-object": {},
            "type": "object",
            "properties": {
                "messages": {
                    "type": "array",
                    "items": {
                        "type": "string"
                    }
                }
            },
            "required": [
                "messages"
            ]
        }
    }
}
whitlockjc commented 5 years ago

Thanks for the report, I get what you're saying and I'll get it sorted.