yamlopenapijsonschemaswagger-jsdocs

Multiple oneOf Groups With OpenAPI 3.0 definition not validating correctly


This is what response body I am trying to represent

[
  {
    measured_at | time_bucket,
    [value] | [latitude, longitude, altitude] | [running, walking]
  }...
]

Above is a bit of a shorthand notation for an array of JSON objects that will all have: the measured_at OR time_bucket, and either value OR latitide,longitude,altitude OR runnning,walking

The closest schema I have written to achieving this is:

measurementsGetDynamicResponse:
      type: array
      items:
        type: object
        allOf:
          # Combine measured_at or time_bucket with one of the measurement data combinations
          - oneOf:
              - properties:
                  measured_at:
                    type: string
                    format: date-time
                    description: "Timestamp of the data point"
              - properties:
                  time_bucket:
                    type: string
                    description: "The time bucket for calculating the aggregation"
          - properties:
              value:
                type: integer
          - properties:
              latitude:
                type: number
                format: float
                description: "Latitude in decimal degrees"
              longitude:
                type: number
                format: float
                description: "Longitude in decimal degrees"
              altitude:
                type: number
                format: float
                description: "Altitude in meters"
          - properties:
              running:
                type: integer
                description: "Number of running steps"
              walking:
                type: integer
                description: "Number of walking steps"

However, this schema doesn't separate the measurement groups, instead combining them all into one group:

(Swagger Example)

[
  {
    "measured_at": "2025-04-01T19:53:13.291Z",
    "value": 0,
    "latitude": 0,
    "longitude": 0,
    "altitude": 0,
    "running": 0,
    "walking": 0
  },
  {
    "time_bucket": "string",
    "value": 0,
    "latitude": 0,
    "longitude": 0,
    "altitude": 0,
    "running": 0,
    "walking": 0
  }
]

I tried grouping the measurement groups in a - oneOf field:

    measurementsGetDynamicResponse:
      type: array
      items:
        type: object
        allOf:
          # Combine measured_at or time_bucket with one of the measurement data combinations
          - oneOf:
              - properties:
                  measured_at:
                    type: string
                    format: date-time
                    description: "Timestamp of the data point"
              - properties:
                  time_bucket:
                    type: string
                    description: "The time bucket for calculating the aggregation"
          - oneOf:
              - properties:
                  value:
                    type: integer
              - properties:
                  latitude:
                    type: number
                    format: float
                    description: "Latitude in decimal degrees"
                  longitude:
                    type: number
                    format: float
                    description: "Longitude in decimal degrees"
                  altitude:
                    type: number
                    format: float
                    description: "Altitude in meters"
              - properties:
                  running:
                    type: integer
                    description: "Number of running steps"
                  walking:
                    type: integer
                    description: "Number of walking steps"

But it kept the measurement group together but separated them from the timestamps

(Swagger Example)

[
  {
    "measured_at": "2025-04-01T19:57:32.452Z"
  },
  {
    "time_bucket": "string"
  },
  {
    "value": 0
  },
  {
    "latitude": 0,
    "longitude": 0,
    "altitude": 0
  },
  {
    "running": 0,
    "walking": 0
  }
]

Any advice on how I can get the schema I am looking for?


Solution

  • Assume you are using OAS 3.0.x, where JSON Schema Draft-04 is necessary

    This ends up being a very verbose schema because you need to constrain additional properties from each oneOf subschema, but also use allOf to create all of the possible values you are seeking.

    each of time_bucket and measured_at must be defined for each oneOf option, then we use oneOf>required to only allow one of those values. Then each set of properties associated to the measurement types are defined with an empty schema because this allows you to use additionalProperties: false as a sibling to allOf.

    $schema: http://json-schema.org/draft-04/schema#
    type: object
    properties:
      measurementsGetDynamicResponse:
        type: array
        items:
          oneOf:
            - $ref: '#/definitions/measurement_and_value'
            - $ref: '#/definitions/measurement_and_workouts'
            - $ref: '#/definitions/measurement_and_geography'
    definitions:
      geographyType:
        type: object
        properties:
          latitude:
            type: number
            format: float
            description: Latitude in decimal degrees
          longitude:
            type: number
            format: float
            description: Longitude in decimal degrees
          altitude:
            type: number
            format: float
            description: Altitude in meters
      workoutType:
        type: object
        properties:
          running:
            type: integer
            description: Number of running steps
          walking:
            type: integer
            description: Number of walking steps
      valueType:
        type: object
        properties:
          value:
            type: integer
      measurementType:
        type: object
        properties:
          measured_at:
            type: string
            format: date-time
            description: Timestamp of the data point
          time_bucket:
            type: string
            description: The time bucket for calculating the aggregation
      measurement_and_value:
        additionalProperties: false
        properties:
          time_bucket: {}
          measured_at: {}
          value: {}
        oneOf:
          - required:
              - measured_at
          - required:
              - time_bucket
        allOf:
          - $ref: "#/definitions/measurementType"
          - $ref: "#/definitions/valueType"
      measurement_and_workouts:
        additionalProperties: false
        properties:
          time_bucket: {}
          measured_at: {}
          running: {}
          walking: {}
        oneOf:
          - required:
              - measured_at
          - required:
              - time_bucket
        allOf:
          - $ref: "#/definitions/measurementType"
          - $ref: "#/definitions/workoutType"
      measurement_and_geography:
        additionalProperties: false
        properties:
          time_bucket: {}
          measured_at: {}
          longitude: {}
          latitude: {}
          altitude: {}
        oneOf:
          - required:
              - measured_at
          - required:
              - time_bucket
        allOf:
          - $ref: "#/definitions/measurementType"
          - $ref: "#/definitions/geographyType"
    
    

    valid examples

    
    {
        "measurementsGetDynamicResponse": [
            {
                "time_bucket": "a time",
                "running": 1,
                "walking": 3
            },
            {
                "measured_at": "2023-01-23",
                "value": 1
            },
            {
                "time_bucket": "test",
                "longitude": 1.00
            }
        ]
    }