OpenAPI 3.0: How to make objects nullable that use oneOf, allOf, or anyOf

OpenAPI 3.0: How to make objects nullable that use oneOf, allOf, or anyOf

File this in the "notes to self" category. The OpenAPI docs are very sparse when it comes to nullable, and it is very unclear how to set an object to nullable when it uses oneOf, allOf, or anyOf in OpenAPI 3.0.

I've been working on writing an OpenAPI spec for an existing API, and hooking it in to Postman for our team to use. Unfortunately, Postman was giving me a validation error when making requests, but all it would say was "The response body didn't match the specified schema", with no actual details on which parts of the schema were wrong.

After much struggle and head-desking, I managed to work it out.

To review, here is how you would make a property nullable:

properties:
  myProperty:
    type: string
    nullable: true

This also works with normal objects:

properties:
  myProperty:
    type: object
    nullable: true
    properties:
      mySubProperty:
        type: string

However, if you have an object that uses oneOf, allOf, or anyOf, setting nullable: true on the object itself will not work.

properties:
  # this works great
  nullableObject:
    type: object
    nullable: true
    properties:
      id:
        type: integer

  # this doesn't work
  notActuallyNullableObjectWithAllOf:
    type: object
    nullable: true
    allOf:
      - type: object
        properties:
          id:
            type: integer
      - type: object
        properties:
          anotherId:
            type: integer

  # neither does this
  notActuallyNullableObjectWithAnyOf:
    type: object
    nullable: true
    anyOf:
      - type: object
        properties:
          id:
            type: integer
      - type: object
        properties:
          anotherId:
            type: integer

The solution depends on your approach and which of oneOf, allOf, or anyOf you are using.

oneOf and anyOf

If you are using oneOf or anyOf, you're going to have the easiest time. As long as at least one of your specified schemas is nullable, the entire object will be nullable.

# oneOf
properties:
  # this won't work
  notActuallyNullableObjectWithOneOf:
    type: object
    nullable: true
    oneOf:
      - type: object
        properties:
          id:
            type: integer
      - type: object
        properties:
          anotherId:
            type: integer

  # neither will this
  notActuallyNullableObjectWithAnyOfRefs:
    type: object
    nullable: true
    anyOf:
      - $ref: '#/components/schemas/SomeReferencedObject'
      - $ref: '#/components/schemas/AnotherReferencedObject'

  # this works great, since nullable: true is set on the first object
  #
  # important: do not include type: object at the top level
  nullableObjectWithOneOf:
    oneOf:
      - type: object
        nullable: true
        properties:
          id:
            type: integer
      - type: object
        properties:
          anotherId:
            type: integer
  nullableObjectWithAnyOf:
    anyOf:
      - type: object
        nullable: true
        properties:
          id:
            type: integer
      - type: object
        properties:
          anotherId:
            type: integer

However, you may run in to a case where you don't want to set nullable: true on one of the specified schemas. This is fairly common when using $ref to reference a schema that is also being used elsewhere.

properties:
  # this won't work
  notActuallyNullableObjectWithOneOfRefs:
    type: object
    nullable: true
    oneOf:
      # I don't want to edit those schemas to set nullable: true, since they're used elsewhere and my _not_ be nullable
      - $ref: '#/components/schemas/SomeReferencedObject'
      - $ref: '#/components/schemas/AnotherReferencedObject'

  # neither will this
  notActuallyNullableObjectWithAnyOfRefs:
    type: object
    nullable: true
    anyOf:
      - $ref: '#/components/schemas/SomeReferencedObject'
      - $ref: '#/components/schemas/AnotherReferencedObject'

The solution here is to add an empty choice to the oneOf or anyOf list, and set nullable: true on that. This becomes a valid choice, and will make the entire object nullable.

properties:
  nullableObjectWithOneOfRefs:
    oneOf:
      # empty nullable object
      - type: object
        nullable: true
      - $ref: '#/components/schemas/SomeReferencedObject'
      - $ref: '#/components/schemas/AnotherReferencedObject'
  nullableObjectWithAnyOfRefs:
    anyOf:
      # empty nullable object
      - type: object
        nullable: true
      - $ref: '#/components/schemas/SomeReferencedObject'
      - $ref: '#/components/schemas/AnotherReferencedObject'

allOf

Getting this to work with allOf is a little bit more complicated. The problem is that allOf requires all of the specified schemas to have nullable: true set, or the entire object will not be nullable.

properties:
  # this won't work
  notActuallyNullableObjectWithAllOf:
    type: object
    nullable: true
    allOf:
      - type: object
        properties:
          id:
            type: integer
      - type: object
        properties:
          anotherId:
            type: integer

  # neither will this, since the secont object is not nullable
  notActuallyNullableObjectWithAllOfAndNullableOptions:
    allOf:
      - type: object
        nullable: true
        properties:
          id:
            type: integer
      - type: object
        properties:
          anotherId:
            type: integer

The solution here is to either set nullable: true on all of the specified schemas, or nest the allOf array inside of a oneOf array, and set nullable: true on the first object in the oneOf array. This approach works just as well for inline schemas as it does for $ref schemas inside the anyOf array.

properties:
  # this works since each `allOf` schema is nullable
  nullableObjectWithAllOfAndNullableOptions:
    allOf:
      - type: object
        nullable: true
        properties:
          id:
            type: integer
      - type: object
        nullable: true
        properties:
          anotherId:
            type: integer

  # this works great as well, without having to adjust the schemas
  nullableObjectWithAllOf:
    anyOf:
      - type: object
        nullable: true
      - type: object
        allOf:
          - type: object
            properties:
              id:
                type: integer
          - type: object
            properties:
              anotherId:
                type: integer

  # the same approach works just as well with `$ref`
  nullableObjectWithAllOfRefs:
    anyOf:
      - type: object
        nullable: true
      - type: object
        allOf:
          - $ref: '#/components/schemas/SomeReferencedObject'
          - $ref: '#/components/schemas/AnotherReferencedObject'

How did I figure this out?

As I said before, Postman was letting me know the responses weren't matching the schema, but it wasn't telling me which part of the schema was wrong.

After a couple days of struggling, I finally found express-openapi-validator. So I set up a quick Express server, and hooked it up to my OpenAPI spec. It gave me a much more detailed error message, and I was able to work out what was wrong and how to fix it.

I figured it might be useful for others to have a little tester that they could run, so I published my test setup on GitHub: openapi-3.0-nullable-object-example. It's a very simple Express server that uses express-openapi-validator to validate the responses against the OpenAPI spec.

Enjoy!