swagger-tools icon indicating copy to clipboard operation
swagger-tools copied to clipboard

Support for polymorphic validation

Open daveismith opened this issue 9 years ago • 44 comments

I've been trying to use swagger-tools to validate requests to my controllers. It seems that when I perform a request with the body parameter specified as a base class, validation happens only against the specification of the base class model, but not against the model for the extended class identified by the discriminator.

daveismith avatar Jul 09 '15 01:07 daveismith

Can you give a code snippet and some example? I ask because we have heavily tested validation so I'm not sure how to reproduce.

whitlockjc avatar Jul 09 '15 04:07 whitlockjc

Below a simplified version of my schema.

{
  "swagger": "2.0",
  "info": {
    "title": "My Cloud API",
    "version": "1.0"
  },
  "consumes": ["application/json"],
  "produces": ["application/json"],
  "host": "localhost:3000",
  "basePath": "/api",
  "paths": {
    "/commands": {
      "post": {
        "description": "Issues a command",
        "operationId": "executeCommand",
        "parameters": [
          {
            "name": "body",
            "in": "body",
            "description": "Command to execute",
            "required": true,
            "schema": {
              "$ref": "#/definitions/Command"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful request.",
            "schema": {
              "$ref": "#/definitions/CommandResult"
            }
          },
          "default": {
            "description": "Invalid request.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          }
        }
      }
    }
  },
  "definitions": {
    "Error": {
      "properties": {
        "message": {
          "type": "string"
        }
      },
      "required": ["message"]
    },
    "CommandResult": {
      "properties": {
        "message": {
          "type": "string"
        }
      },
      "required": ["message"]
    },
    "Command": {
      "discriminator": "type",
      "properties": {
        "type": {
          "type": "string"
        },
        "propBase": {
          "type": "string"
        }
      },
      "required": ["type", "propBase"]
    },
    "Command1": {
      "description": "Command 1",
      "allOf": [
        {
          "$ref": "#/definitions/Command"
        },
        {
          "properties": {
            "prop1": {
              "type": "string"
            }
          },
          "required": ["prop1"]
        }
      ]
    }
  }
}

The issue is that commands that are sent to /api/commands are only validated against Command, regardless of the discriminator value. If I send the following it will still pass validation and call my controller function even though it's missing the required property "prop1" for the type "Command1".

{
  "type": "Command1",
  "propBase": "blah"
}

daveismith avatar Jul 09 '15 13:07 daveismith

I cannot reproduce this. Whenever I throw your definitions into my API and have my request/response use them, if I post only what you do, I get a request validation error (Parameter (command) failed schema validation]):

{
  [Error: Parameter (pet) failed schema validation]
  code: 'SCHEMA_VALIDATION_FAILED',
  failedValidation: true,
  results: {
    errors: [
      {
        "code": "OBJECT_MISSING_REQUIRED_PROPERTY",
        "message": "Missing required property: prop1",
        "path": []
      }
    ],
    warnings: []
  },
  path: [ 'paths', '/pets', 'post', 'parameters', '0' ],
  paramName: 'pet'
}

And when I post a full example, with the prop1 provided, the request is successful. Which version of swagger-tools are you using?

whitlockjc avatar Jul 09 '15 15:07 whitlockjc

My package.json has "swagger-tools": "^0.8.7". I am using it with MEAN.js, and have the following in my express.js configuration file. I'm using the express router vs the swagger-tools one, but it since that middleware comes after the validator, and it's validating some requests I'm hope I've got it correctly configured.

    var swaggerDoc = require('../app/api/swagger.json');

    swaggerTools.initializeMiddleware(swaggerDoc, function (middleware) {
        // Interpret Swagger resources and attach metadata to request - must be first in swagger-tools middleware chain
        app.use(middleware.swaggerMetadata());

        // Validate Swagger requests
        app.use(middleware.swaggerValidator());

        // Route validated requests to appropriate controller
        //app.use(middleware.swaggerRouter(options));

        // Serve the Swagger documents and Swagger UI
        app.use(middleware.swaggerUi());
    });

daveismith avatar Jul 09 '15 15:07 daveismith

Can you run with DEBUG=swagger-tools* and paste the output of the failed request? swagger-validator should be working right based on my tests.

whitlockjc avatar Jul 09 '15 15:07 whitlockjc

When I submit

{
  "type": "Command1",
  "propBase": "string"
}

the output of the request to the log is

NODE_ENV is not defined! Using default development environment
  swagger-tools:middleware Initializing middleware +0ms
  swagger-tools:middleware   Identified Swagger version: 2.0 +2ms
  swagger-tools:middleware   Validation: succeeded +26ms
  swagger-tools:middleware:metadata Initializing swagger-metadata middleware +162ms
  swagger-tools:middleware:metadata   Identified Swagger version: 2.0 +1ms
  swagger-tools:middleware:metadata   Found Path: /commands +1ms
  swagger-tools:middleware:validator Initializing swagger-validator middleware +1ms
  swagger-tools:middleware:validator   Response validation: disabled +0ms
  swagger-tools:middleware:ui Initializing swagger-ui middleware +35ms
  swagger-tools:middleware:ui   Using swagger-ui from: internal +1ms
  swagger-tools:middleware:ui   API Docs path: /api-docs +0ms
  swagger-tools:middleware:ui   swagger-ui path: /docs +0ms
MEAN.JS application started on port 3000
  swagger-tools:middleware:metadata POST /api/commands +2m
  swagger-tools:middleware:metadata   Is a Swagger path: true +1ms
  swagger-tools:middleware:metadata   Is a Swagger operation: true +0ms
  swagger-tools:middleware:metadata   Processing Parameters +0ms
  swagger-tools:middleware:metadata     body +1ms
  swagger-tools:middleware:metadata       Type: object +1ms
  swagger-tools:middleware:metadata       Value provided: true +0ms
  swagger-tools:middleware:metadata       Value: [object Object] +0ms
  swagger-tools:middleware:validator POST /api/commands +0ms
  swagger-tools:middleware:validator   Will process: yes +0ms
  swagger-tools:middleware:ui POST /api/commands +5ms
  swagger-tools:middleware:ui   Will process: no +0ms
POST /api/commands 200 159.372 ms - 4

daveismith avatar Jul 09 '15 15:07 daveismith

Can you show me the rest of your server file, replacing sensitive data with reasonable mocks so as not to change the structure of the file?

whitlockjc avatar Jul 09 '15 16:07 whitlockjc

I hate to keep bugging you but I cannot reproduce it. If you'd rather not give me your server file, could you give me a recipe via a patch/PR that will showcase this issue in test/2.0/test-middleware-swagger-validator.js. Without a way to reproduce this, I cannot fix it.

whitlockjc avatar Jul 09 '15 16:07 whitlockjc

I was able to reproduce it. It required me to get outside of the test suite. Let me take a look.

whitlockjc avatar Jul 09 '15 17:07 whitlockjc

Awesome. I was just about to start putting together a skeleton app for you. Sorry for the delay. I was out grabbing some lunch. Let me know if you need the skeleton app anymore.

daveismith avatar Jul 09 '15 17:07 daveismith

I've figured it out, swagger-tools is working right which is why my test worked but the server didn't. Your parameter references #/definitions/Command but you want it to be #/definitions/Command1. If I change the reference, it works as expected.

whitlockjc avatar Jul 09 '15 17:07 whitlockjc

Right, but the goal in reality is that I want to have Command1, Command2, Command3, etc. Is that not possible?

daveismith avatar Jul 09 '15 17:07 daveismith

Let me get @webron involved as I don't understand the discriminator field honestly, I thought it was only useful for swagger-codegen. I would expect that allowing a parameter, or a response, to be N different types would violate Swagger's design goals but let's see what Ron has to say.

whitlockjc avatar Jul 09 '15 17:07 whitlockjc

Cool. I'm pretty new to swagger, so I may be understanding it incorrectly. I'll wait to hear what the verdict is. Thanks for being so responsive.

daveismith avatar Jul 09 '15 17:07 daveismith

I'm guessing TL;DR won't work here? :blush:

Let me have a look.

webron avatar Jul 09 '15 17:07 webron

Okay, I hope I got everything needed from the thread. If the intention is to pass Command as a (body) parameter, and Command has a discriminator, then indeed anything that adds Command to it (via allOf) becomes a valid input (and this is recursive). So in this case, both Command and Command1 would be valid as input for the operation.

Two side notes - I'm happy to see that you've used the discriminator properly. Describing it is a bit complicated, and you've hit both spots - the discriminator is required and the sub-Command overrides the discriminator with a specific value (seems natural, but it's very easy to miss).

The other, which is slightly unrelated, for every model you should include "type": "object". While missing it will still work, technically, it allows things you don't want to allow.

webron avatar Jul 09 '15 18:07 webron

Let me get this straight: If your model has an ancestor that uses the discriminator, that model is a valid value anywhere the ancestor is used as a type?

whitlockjc avatar Jul 09 '15 19:07 whitlockjc

That was my understanding. Think of it like a function in a language with strong typing like Java or C++. I can declare a function that receives a base class or interface, and any function that uses that base class can also accept any subclass too.

daveismith avatar Jul 09 '15 19:07 daveismith

@whitlockjc - that is correct and the explanation @daveismith provided is fairly accurate. The discriminator is used to declare inheritance between models (unlike the simple usage of allOf that's basically composition but offers not hierarchical relationship between the models). So whenever you use the parent definition, its descendants could be used as well. Hope that's clear.

webron avatar Jul 09 '15 19:07 webron

Wow...that changes all kinds of things.

whitlockjc avatar Jul 09 '15 20:07 whitlockjc

@webron - So to support this, I would need to see if a schema had a discriminator and if so, find all of the ancestors of the model. For each of these ancestors, validate the provided value against its schema and if any of those ancestor schemas validate successfully, validation succeeds. If all of them fail, validation fails. Is that correct?

whitlockjc avatar Jul 09 '15 21:07 whitlockjc

Wouldn't it be the reverse? If the specified schema had a discriminator you would need to look for any schemas that have an allOf that specifies a reference to the schema in question.

daveismith avatar Jul 09 '15 21:07 daveismith

Ah yes, that is correct.

whitlockjc avatar Jul 09 '15 21:07 whitlockjc

Actually, it's not even that complex. When you receive an object that is specified by a schema with a discriminator you would just look at the value of the discriminator field to determine the name of the schema that should be used (hopefully not too much more complex that what's already happening).

daveismith avatar Jul 09 '15 21:07 daveismith

It's not a complexity thing to understand, just a potentially complex thing to implement due to how we do validation right now. I could be wrong, it might be really simple.

whitlockjc avatar Jul 09 '15 21:07 whitlockjc

@webron mentioned recursion. I'm not 100% on that, I can think of two cases.

  • subclass specifies another, different discriminator?
  • a class specifies the subclass in an allOf, then the discriminator would inform you of the final class directly

daveismith avatar Jul 09 '15 21:07 daveismith

Can you join #swagger on irc.freenode.net? It might be easier to chat in real time there.

whitlockjc avatar Jul 09 '15 21:07 whitlockjc

I've got to head out now, but could tomorrow.

daveismith avatar Jul 09 '15 21:07 daveismith

Please do. I think I understand it but my understanding doesn't sound simple so I could be misunderstanding it. I'm jeremyw so highlight me and I'll respond.

whitlockjc avatar Jul 09 '15 21:07 whitlockjc

When I was talking about recursion, there are two cases:

  • There's a model (ModelX) with allOf parent that doesn't specify the discriminator value (composition), and then there are models that allOf ModelX, and do specify the discriminator value. That's still inheritance.
  • (and this one is less certain) - There's a model (ModelY) with allOf parent that does specify the discriminator value (inheritance), and then are models that allOf ModelY. That's a composition of an inherited model. I'm not sure you want to deal with this use case for now and the meaning of it would be uncertain.

webron avatar Jul 10 '15 09:07 webron