[v2.0] Request & reponse bodies fail validation when using oneOf to specify alternate schemas

Issue #143 resolved
Bill Bruyn created an issue

I believe the following payload conforms to my pretend schema, but fails validation with the attached stacktrace...

openapi: "3.0.1"
info:
  version: 1.0.0
  title: My Application
servers:
  - url: http://localhost:8080/api

paths:
  /pets:
    post:
      summary: Add a new pet
      requestBody:
        description: A JSON object containing pet information
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/Cat'
                - $ref: '#/components/schemas/Dog'
      responses:
        '201':
          description: Created
components:
  schemas:
    Cat:
      type: object
      properties:
        scientificName:
          type: string
        meow:
          type: string
    Dog:
      type: object
      properties:
        scientificName: 
          type: string
        bark:
          type: string
{
    "scientificName":  "Canis lupus familiaris",
    "bark": "very loud"
}
    @Test
    public void testAdd() throws IOException {

        InputStream json = getClass().getResourceAsStream("/pets-add.json");

        given()
            .filter(filter)
        .when()
                .body(IOUtils.toString(json, "UTF-8"))
                .contentType(ContentType.JSON)
                .post("/Pets")
        .then()
                .log().all()
                .assertThat()
                .statusCode(200)
                .contentType(ContentType.JSON);
    }
com.atlassian.oai.validator.restassured.SwaggerValidationFilter$SwaggerValidationException: Validation failed.
[ERROR] Object instance has properties which are not allowed by the schema: ["bark","scientificName"]
    at com.atlassian.oai.validator.restassured.SwaggerValidationFilter.filter(SwaggerValidationFilter.java:61)
    at io.restassured.filter.Filter$filter.call(Unknown Source)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:113)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:141)
    at io.restassured.internal.filter.FilterContextImpl.next(FilterContextImpl.groovy:72)
    at io.restassured.filter.FilterContext$next.call(Unknown Source)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:113)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:133)
    at io.restassured.internal.RequestSpecificationImpl.applyPathParamsAndSendRequest(RequestSpecificationImpl.groovy:1731)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
    at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
    at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1213)
    at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1022)
    at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:810)
    at io.restassured.internal.RequestSpecificationImpl.invokeMethod(RequestSpecificationImpl.groovy)
    at org.codehaus.groovy.runtime.callsite.PogoInterceptableSite.call(PogoInterceptableSite.java:48)
    at org.codehaus.groovy.runtime.callsite.PogoInterceptableSite.callCurrent(PogoInterceptableSite.java:58)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallCurrent(CallSiteArray.java:52)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:154)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:182)
    at io.restassured.internal.RequestSpecificationImpl.applyPathParamsAndSendRequest(RequestSpecificationImpl.groovy:1737)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
    at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
    at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1213)
    at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1022)
    at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:810)
    at io.restassured.internal.RequestSpecificationImpl.invokeMethod(RequestSpecificationImpl.groovy)
    at org.codehaus.groovy.runtime.callsite.PogoInterceptableSite.call(PogoInterceptableSite.java:48)
    at org.codehaus.groovy.runtime.callsite.PogoInterceptableSite.callCurrent(PogoInterceptableSite.java:58)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallCurrent(CallSiteArray.java:52)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:154)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:182)
    at io.restassured.internal.RequestSpecificationImpl.post(RequestSpecificationImpl.groovy:174)
    at io.restassured.internal.RequestSpecificationImpl.post(RequestSpecificationImpl.groovy)
    at com.atlassian.oai.validator.examples.restassured.PetOperationsTest.testAdd(PetOperationsTest.java:35)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:538)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:760)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:460)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:206)

Comments (15)

  1. Bill Bruyn reporter

    Note that the same seems to be true with response bodies. e.g., the following response passes when replacing the "oneOf" block with either of its items

    #!
    
          responses:
            '200':
              description: OK
              content:
                'application/json':
                  schema:
                    type: array
                    items:
    
                      # this passes
    
                      $ref: "#/components/schemas/ResponseBodyModel"
    
                      # this passes
    
                      # - type: array
                      #   items:
                      #     $ref: "#/components/schemas/ResponseBodyModel"
    
                      # this fails
    
                      # oneOf:
                      #   - $ref: "#/components/schemas/ResponseBodyModel"
                      #   - type: array
                      #     items:
                      #       $ref: "#/components/schemas/ResponseBodyModel"
    
  2. James Navin

    Thanks for raising this. I haven't done any work to support oneOf or anyOf as yet, but its on the backlog.

  3. James Navin

    I think this is the same root cause as the allOf problem mentioned in the FAQ section of the README. The problem is to do with how JSON Schema composition plays with the additionalProperties: false directive (which is automatically inserted by the validator).

    https://bitbucket.org/atlassian/swagger-request-validator/pull-requests/105 has tests that exercise the anyOf, oneOf and allOf composition - all require disabling the additionalProperties validation.

    Can you try disabling the additional properties validation and see if that fixes your problem (see the FAQ for details on how to do that).

  4. Bill Bruyn reporter

    That changes the error, but still seems to result in a failure:

    #!
    
    com.atlassian.oai.validator.restassured.SwaggerValidationFilter$SwaggerValidationException: Validation failed.
    [ERROR] Instance failed to match exactly one schema (matched 2 out of 2)
        at com.atlassian.oai.validator.restassured.SwaggerValidationFilter.filter(SwaggerValidationFilter.java:61)
        at io.restassured.filter.Filter$filter.call(Unknown Source)
        at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:113)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:141)
        at io.restassured.internal.filter.FilterContextImpl.next(FilterContextImpl.groovy:72)
        at io.restassured.filter.FilterContext$next.call(Unknown Source)
        at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:113)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:133)
        at io.restassured.internal.RequestSpecificationImpl.applyPathParamsAndSendRequest(RequestSpecificationImpl.groovy:1731)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:483)
        at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
        at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
        at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1213)
        at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1022)
        at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:810)
        at io.restassured.internal.RequestSpecificationImpl.invokeMethod(RequestSpecificationImpl.groovy)
        at org.codehaus.groovy.runtime.callsite.PogoInterceptableSite.call(PogoInterceptableSite.java:48)
        at org.codehaus.groovy.runtime.callsite.PogoInterceptableSite.callCurrent(PogoInterceptableSite.java:58)
        at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallCurrent(CallSiteArray.java:52)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:154)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:182)
        at io.restassured.internal.RequestSpecificationImpl.applyPathParamsAndSendRequest(RequestSpecificationImpl.groovy:1737)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:483)
        at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
        at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
        at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1213)
        at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1022)
        at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:810)
        at io.restassured.internal.RequestSpecificationImpl.invokeMethod(RequestSpecificationImpl.groovy)
        at org.codehaus.groovy.runtime.callsite.PogoInterceptableSite.call(PogoInterceptableSite.java:48)
        at org.codehaus.groovy.runtime.callsite.PogoInterceptableSite.callCurrent(PogoInterceptableSite.java:58)
        at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallCurrent(CallSiteArray.java:52)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:154)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:182)
        at io.restassured.internal.RequestSpecificationImpl.post(RequestSpecificationImpl.groovy:174)
        at io.restassured.internal.RequestSpecificationImpl.post(RequestSpecificationImpl.groovy)
        at com.atlassian.oai.validator.examples.restassured.CustomerOperationsTest.testAdd(CustomerOperationsTest.java:47)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:483)
        at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
        at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
        at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
        at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
        at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
        at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
        at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
        at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
        at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
        at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
        at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
        at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
        at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
        at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:538)
        at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:760)
        at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:460)
        at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:206)
    
  5. James Navin

    That does seem odd.

    The SwaggerValidationException that's being generated should include a copy of the validation report on it. Can you try catching the exception and the logging the full details of the report with e.g.

    catch (SwaggerValidationException e) {
       log.info(JsonValidationReportFormat.getInstance().apply(e.getValidationReport());
    }
    
  6. Bill Bruyn reporter
    #!
    
    {
      "messages" : [ {
        "key" : "validation.request.body.schema.oneOf",
        "level" : "ERROR",
        "message" : "Instance failed to match exactly one schema (matched 2 out of 2)",
        "context" : {
          "requestPath" : "/api/Customers",
          "apiRequestContentType" : "application/json",
          "location" : "REQUEST",
          "requestMethod" : "POST"
        }
      } ]
    }
    
  7. James Navin

    Im still struggling to reproduce the problem.

    Perhaps if you could give an example schema and request that shows the problem I can use that directly in my tests.

  8. Bill Bruyn reporter

    An example schema, payload, and test method are in the original post... are you saying that you see a different behavior with versions 2.0.0-RC2 and RC3?

  9. Bill Bruyn reporter

    For clarity:

    Example Schema (http://localhost:8080/api/Pets.yaml):

    #!
    
    openapi: "3.0.1"
    info:
      version: 1.0.0
      title: My Application
    servers:
      - url: http://localhost:8080/api
    
    paths:
      /Pets:
        post:
          summary: Add a new pet
          requestBody:
            description: A JSON object containing pet information
            content:
              application/json:
                schema:
                  oneOf:
                    - $ref: '#/components/schemas/Cat'
                    - $ref: '#/components/schemas/Dog'
          responses:
            '201':
              description: Created
            default:
              description: Error
    components:
      schemas:
        Cat:
          type: object
          properties:
            scientificName:
              type: string
            meow:
              type: string
        Dog:
          type: object
          properties:
            scientificName: 
              type: string
            bark:
              type: string
    

    Example request (com.atlassian.oai.validator.restassured.PetsOperationsTest.testAdd() - note by the way that OpenApiValidationException has default visibility so I had to change my own packaging to make that go. Separate bug there?)

    package com.atlassian.oai.validator.restassured;
    
    import static io.restassured.RestAssured.given;
    
    import java.io.IOException;
    import java.io.InputStream;
    
    import org.apache.commons.io.IOUtils;
    import org.junit.Test;
    
    import com.atlassian.oai.validator.OpenApiInteractionValidator;
    import com.atlassian.oai.validator.report.JsonValidationReportFormat;
    import com.atlassian.oai.validator.report.LevelResolver;
    import com.atlassian.oai.validator.report.ValidationReport.Level;
    import com.atlassian.oai.validator.restassured.OpenApiValidationFilter.OpenApiValidationException;
    
    import io.restassured.RestAssured;
    import io.restassured.http.ContentType;
    
    public class PetsOperationsTest {
    
        private static final String BASE_URI = "http://localhost:8080/api";
        private static final String SPEC_URI = BASE_URI + "/Pets.yaml";
        private static final OpenApiInteractionValidator validator = OpenApiInteractionValidator
            .createFor(SPEC_URI)
            .withLevelResolver(
                LevelResolver.create().withLevel("validation.schema.additionalProperties", Level.IGNORE).build())
            .build();
        private static final OpenApiValidationFilter filter = new OpenApiValidationFilter(validator);
    
        static {
                RestAssured.baseURI = BASE_URI;
        }
    
    
        @Test
        public void testAdd() throws IOException {
    
                InputStream json = getClass().getResourceAsStream("/pets-add.json");
                try {
    
                    given()
                        .filter(filter)
                    .when()
                            .body(IOUtils.toString(json, "UTF-8"))
                            .contentType(ContentType.JSON)
                            .post("/Pets")
                    .then()
                            .log().all()
                            .assertThat()
                            .statusCode(200)
                            .contentType(ContentType.JSON);
    
                } catch (OpenApiValidationException e) {
               System.out.println((JsonValidationReportFormat.getInstance().apply(e.getValidationReport())));
            }
        }
    
    }
    

    Example payload (/pets-add.json)

    #!
    
    
    {
        "scientificName":  "Canis lupus familiaris",
        "bark": "very loud"
    }
    

    Result

    #!
    
    {
      "messages" : [ {
        "key" : "validation.request.body.schema.oneOf",
        "level" : "ERROR",
        "message" : "Instance failed to match exactly one schema (matched 2 out of 2)",
        "context" : {
          "requestPath" : "/api/Pets",
          "apiRequestContentType" : "application/json",
          "location" : "REQUEST",
          "requestMethod" : "POST"
        }
      } ]
    }
    
  10. James Navin

    Ah - I think I see now. Neither of the Dog or Cat schemas define required properties, so the request you’re making technically matches both schemas and so fails validation.

    Try making the properties required in both schemas and see if that fixes the problem.

    And yes - the visibility of the exception is a bug - I’ll fix that next week (thanks for pointing it out!)

  11. Bill Bruyn reporter

    Marked 'bark' and 'meow' properties required, and that (along with disabling the additionalProperties validation suggested above) works around it.

  12. Log in to comment