Search code examples
openapi

OpenAPI validation error for complex message using discriminator


Anyone knows if there's any good online site for validating JSON messages against an OpenAPI 3.0.x document?

I've got the following signature for a path:

 /api/equipamentos:
    post:
...
    put:
      requestBody:
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/MsgAtualizacaoComputador'
                - $ref: '#/components/schemas/MsgAtualizacaoEquipamentoAtivo'
                - $ref: '#/components/schemas/MsgAtualizacaoEquipamentoGenerico'
                - $ref: '#/components/schemas/MsgAtualizacaoImpressora'
                - $ref: '#/components/schemas/MsgAtualizacaoSoftware'
...

Where the types look like this:

    MsgAtualizacaoEquipamento:
      required:
        - $type
      type: object
      properties:
        $type:
          type: string
        serialNumber:
          type: string
          nullable: true
        tag:
          type: string
          nullable: true
        modelo:
          type: string
          nullable: true
        username:
          type: string
          nullable: true
        partNumber:
          type: string
          nullable: true
        idFabricante:
          type: integer
          format: int32
        dataAquisicao:
          type: string
          format: date-time
        dataFimGarantia:
          type: string
          format: date-time
        data:
          type: string
          format: date-time
        idFornecedor:
          type: integer
          format: int32
        observacoes:
          type: string
          nullable: true
        estadoEquipamento:
          $ref: '#/components/schemas/EstadoEquipamento'
        idLocalTrabalho:
          type: integer
          format: int32
        idFuncionario:
          type: integer
          format: int32
          nullable: true
        localizacaoArmazem:
          type: string
          nullable: true
        importGuid:
          type: string
          format: uuid
        etiquetaImpressaEm:
          type: string
          format: date-time
          nullable: true
        dataRetoma:
          type: string
          format: date-time
          nullable: true
        idImportacao:
          type: integer
          format: int32
        dadosGerfip:
          $ref: '#/components/schemas/DadosGerfipv2'
        id:
          type: integer
          format: int32
        version:
          type: integer
          format: int32
      additionalProperties: false
      discriminator:
        propertyName: $type
        mapping:
          Sra.Assistencias.Dtos.Equipamentos.MsgAtualizacaoComputador, Sra.Assistencias.Dtos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: '#/components/schemas/MsgAtualizacaoComputador'
          Sra.Assistencias.Dtos.Equipamentos.MsgAtualizacaoEquipamentoAtivo, Sra.Assistencias.Dtos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: '#/components/schemas/MsgAtualizacaoEquipamentoAtivo'
          Sra.Assistencias.Dtos.Equipamentos.MsgAtualizacaoEquipamentoGenerico, Sra.Assistencias.Dtos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: '#/components/schemas/MsgAtualizacaoEquipamentoGenerico'
          Sra.Assistencias.Dtos.Equipamentos.MsgAtualizacaoImpressora, Sra.Assistencias.Dtos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: '#/components/schemas/MsgAtualizacaoImpressora'
          Sra.Assistencias.Dtos.Equipamentos.MsgAtualizacaoSoftware, Sra.Assistencias.Dtos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: '#/components/schemas/MsgAtualizacaoSoftware'
    MsgAtualizacaoEquipamentoAtivo:
      type: object
      allOf:
        - $ref: '#/components/schemas/MsgAtualizacaoEquipamento'
      properties:
        nome:
          type: string
          nullable: true
        numPortas:
          type: integer
          format: int32
        ip:
          type: string
          nullable: true
        velocidade:
          type: string
          nullable: true
        idTipoEquipamentoAtivo:
          type: integer
          format: int32
      additionalProperties: false
    MsgAtualizacaoEquipamentoComEquipamentosAssociados:
      required:
        - $type
      type: object
      properties:
        $type:
          type: string
        serialNumber:
          type: string
          nullable: true
        tag:
          type: string
          nullable: true
        modelo:
          type: string
          nullable: true
        username:
          type: string
          nullable: true
        partNumber:
          type: string
          nullable: true
        idFabricante:
          type: integer
          format: int32
        dataAquisicao:
          type: string
          format: date-time
        dataFimGarantia:
          type: string
          format: date-time
        data:
          type: string
          format: date-time
        idFornecedor:
          type: integer
          format: int32
        observacoes:
          type: string
          nullable: true
        estadoEquipamento:
          $ref: '#/components/schemas/EstadoEquipamento'
        idLocalTrabalho:
          type: integer
          format: int32
        idFuncionario:
          type: integer
          format: int32
          nullable: true
        localizacaoArmazem:
          type: string
          nullable: true
        importGuid:
          type: string
          format: uuid
        etiquetaImpressaEm:
          type: string
          format: date-time
          nullable: true
        dataRetoma:
          type: string
          format: date-time
          nullable: true
        idImportacao:
          type: integer
          format: int32
        dadosGerfip:
          $ref: '#/components/schemas/DadosGerfipv2'
        id:
          type: integer
          format: int32
        version:
          type: integer
          format: int32
        idsEquipamentosAssociar:
          type: array
          items:
            $ref: '#/components/schemas/EquipamentoInfo'
          nullable: true
        idsEquipamentosCancelar:
          type: array
          items:
            $ref: '#/components/schemas/EquipamentoInfo'
          nullable: true
        idsEquipamentosPropagar:
          type: array
          items:
            $ref: '#/components/schemas/EquipamentoInfo'
          nullable: true
      additionalProperties: false
      discriminator:
        propertyName: $type
        mapping:
          Sra.Assistencias.Dtos.Equipamentos.MsgAtualizacaoComputador, Sra.Assistencias.Dtos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: '#/components/schemas/MsgAtualizacaoComputador'
          Sra.Assistencias.Dtos.Equipamentos.MsgAtualizacaoSoftware, Sra.Assistencias.Dtos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: '#/components/schemas/MsgAtualizacaoSoftware'
    MsgAtualizacaoEquipamentoGenerico:
      type: object
      allOf:
        - $ref: '#/components/schemas/MsgAtualizacaoEquipamento'
      properties:
        idTipoEquipamentoGenerico:
          type: integer
          format: int32
      additionalProperties: false
    MsgAtualizacaoEquipamentos:
      type: object
      properties:
        observacoes:
          type: string
          nullable: true
        bytes:
          type: string
          format: byte
          nullable: true
      additionalProperties: false
    MsgAtualizacaoFuncionario:
      type: object
      properties:
        idFuncionario:
          type: integer
          format: int32
        version:
          type: integer
          format: int32
        nif:
          type: string
          nullable: true
        idLocalTrabalho:
          type: integer
          format: int32
        nome:
          type: string
          nullable: true
        carreira:
          type: string
          nullable: true
        numeroMecanografico:
          type: integer
          format: int32
        codUnidadeOrganica:
          type: integer
          format: int32
        situacaoProfissional:
          $ref: '#/components/schemas/SituacaoProfissional'
        contactos:
          type: array
          items:
            $ref: '#/components/schemas/Contacto'
          nullable: true
        propagaLocalTrabalhoParaEquipamentos:
          type: boolean
      additionalProperties: false
    MsgAtualizacaoImpressora:
      type: object
      allOf:
        - $ref: '#/components/schemas/MsgAtualizacaoEquipamento'
      properties:
        idTipoImpressora:
          type: integer
          format: int32
        ip:
          type: string
          nullable: true
        nomeNetBIOS:
          type: string
          nullable: true
      additionalProperties: false
    MsgAtualizacaoSoftware:
      type: object
      allOf:
        - $ref: '#/components/schemas/MsgAtualizacaoEquipamentoComEquipamentosAssociados'
      properties:
        numLicencasAdquiridas:
          type: integer
          format: int32
        nome:
          type: string
          nullable: true
        versao:
          type: string
          nullable: true
        upgrade:
          type: boolean
      additionalProperties: false

We simply can't pass any message without getting validation errors. For instance, here's a sample which can't pass the validation test:

{
    "$type": "Sra.Assistencias.Dtos.Equipamentos.MsgAtualizacaoComputador, Sra.Assistencias.Dtos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
    "nomeNetBIOS": "XPTO",
    "ip": "",
    "processador": "IU7-155H",
    "memoria": "16GB",
    "disco": "512GB",
    "gateway": "",
    "idTipoComputador": 3,
    "idsEquipamentosAssociar": [],
    "idsEquipamentosCancelar": [],
    "idsEquipamentosPropagar": [],
    "id": 83695,
    "version": 6,
    "serialNumber": "PWOCWVQD",
    "tag": "TAG000082196",
    "modelo": "Lenovo too",
    "username": "",
    "partNumber": "21MR004BPG",
    "idFabricante": 153,
    "dataAquisicao": "2024-12-16T00:00:00+00:00",
    "dataFimGarantia": "2027-12-15T00:00:00+00:00",
    "data": "0001-01-01T00:00:00",
    "idFornecedor": 57,
    "observacoes": "None",
    "estadoEquipamento": 4,
    "idLocalTrabalho": 807,
    "idFuncionario": 35112,
    "localizacaoArmazem": "",
    "importGuid": "00000000-0000-0000-0000-000000000000",
    "etiquetaImpressaEm": "2025-01-07T10:27:25.6946923+00:00",
    "dataRetoma": null,
    "idImportacao": 0,
    "dadosGerfip": {
        "$type": "Sra.Assistencias.Dtos.Equipamentos.DadosGerfipv2, Sra.Assistencias.Dtos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
        "procedimentoAquisitivo": "INTERREG VIU-D MAC 2121-2027",
        "numeroInventario": "1000310010",
        "dataImportacaoInventario": null
    }
}

I've already checked and triple checked the JSON message and I can't see anything wrong with it. Any ideas on what's going on?


Solution

  • Seems you have run into a very common mistake with JSON Schema, particularly older draft versions like the one used in OpenAPI 3.0.x (JSON Schema draft-04)

    allOf and additionalProperties: false don't really play nice together.

    You can see an explanation here https://stackoverflow.com/a/79302728/8564731

    Furthermore, your allOf construction is malformed, if I am interpreting your intent correctly. All subschemas with an allOf should be inside the array. You have defined allOf and a sibling properties keyword, which is valid JSON Schema, but I don't think that's how you were intending to use it.

        allOf:
            - $ref: '#/components/schemas/MsgAtualizacaoEquipamento'
          properties:
            nome:
              type: string
              nullable: true
            numPortas:
              type: integer
              format: int32
            ip:
              type: string
              nullable: true
            velocidade:
              type: string
              nullable: true
            idTipoEquipamentoAtivo:
              type: integer
              format: int32
          additionalProperties: false
    
         allOf:
            - $ref: "#/definitions/schemas/MsgAtualizacaoEquipamento"
            - properties: # << this should be part of the array
                nome:
                  type: string
                  nullable: true
                numPortas:
                  type: integer
                  format: int32
                ip:
                  type: string
                  nullable: true
                velocidade:
                  type: string
                  nullable: true
                idTipoEquipamentoAtivo:
                  type: integer
                  format: int32
    

    additionalProperties: false should be removed from your schemas. If you really want to constraint your schemas not to allow additional properties, you need to follow the advice of the post I referenced, where you must redefine every possible keyword at the root of the schema. Then you can apply additionalProperties: false.


    The last issue, after you fix those items is the discriminator mapping should be a sibling to the oneOf in your request body.

    The example provided doesn't match the available schemas you have given us so I had to modify it to make it work correctly.

    There are quite a few missing schemas to make it function 100%, but hopefully that gives you an idea of how to fix it.

    openapi: 3.0.4
    info:
      title: test
      version: 1.0.0
    servers: []
    paths:
      /thing:
        post:
          summary: a request
          responses:
            '200':
              description: OK
          requestBody:
            content:
              application/json:
                schema:
                  oneOf:
                    - $ref: '#/components/schemas/MsgAtualizacaoEquipamentoAtivo'
                    - $ref: '#/components/schemas/MsgAtualizacaoEquipamentoGenerico'
                    - $ref: '#/components/schemas/MsgAtualizacaoImpressora'
                    - $ref: '#/components/schemas/MsgAtualizacaoSoftware'
                  discriminator:
                    propertyName: $type
                    mapping:
                      Sra.Assistencias.Dtos.Equipamentos.MsgAtualizacaoEquipamentoAtivo, Sra.Assistencias.Dtos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: '#/components/schemas/MsgAtualizacaoEquipamentoAtivo'
                      Sra.Assistencias.Dtos.Equipamentos.MsgAtualizacaoEquipamentoGenerico, Sra.Assistencias.Dtos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: '#/components/schemas/MsgAtualizacaoEquipamentoGenerico'
                      Sra.Assistencias.Dtos.Equipamentos.MsgAtualizacaoImpressora, Sra.Assistencias.Dtos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: '#/components/schemas/MsgAtualizacaoImpressora'
                      Sra.Assistencias.Dtos.Equipamentos.MsgAtualizacaoSoftware, Sra.Assistencias.Dtos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: '#/components/schemas/MsgAtualizacaoSoftware'
                examples:
                  test:
                    value:
                      {
                        '$type': 'Sra.Assistencias.Dtos.Equipamentos.MsgAtualizacaoEquipamentoAtivo, Sra.Assistencias.Dtos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null',
                        'nomeNetBIOS': 'XPTO',
                        'ip': '',
                        'processador': 'IU7-155H',
                        'memoria': '16GB',
                        'disco': '512GB',
                        'gateway': '',
                        'idTipoComputador': 3,
                        'idsEquipamentosAssociar': [],
                        'idsEquipamentosCancelar': [],
                        'idsEquipamentosPropagar': [],
                        'id': 83695,
                        'version': 6,
                        'serialNumber': 'PWOCWVQD',
                        'tag': 'TAG000082196',
                        'modelo': 'Lenovo too',
                        'username': '',
                        'partNumber': '21MR004BPG',
                        'idFabricante': 153,
                        'dataAquisicao': '2024-12-16T00:00:00+00:00',
                        'dataFimGarantia': '2027-12-15T00:00:00+00:00',
                        'data': '0001-01-01T00:00:00',
                        'idFornecedor': 57,
                        'observacoes': 'None',
                        'estadoEquipamento': 4,
                        'idLocalTrabalho': 807,
                        'idFuncionario': 35112,
                        'localizacaoArmazem': '',
                        'importGuid': '00000000-0000-0000-0000-000000000000',
                        'etiquetaImpressaEm': '2025-01-07T10:27:25.6946923+00:00',
                        'dataRetoma': null,
                        'idImportacao': 0,
                        'dadosGerfip':
                          {
                            '$type': 'Sra.Assistencias.Dtos.Equipamentos.DadosGerfipv2, Sra.Assistencias.Dtos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null',
                            'procedimentoAquisitivo': 'INTERREG VIU-D MAC 2121-2027',
                            'numeroInventario': '1000310010',
                            'dataImportacaoInventario': null,
                          },
                      }
    components:
      schemas:
        MsgAtualizacaoEquipamento:
          required:
            - $type
          type: object
          properties:
            $type:
              type: string
            serialNumber:
              type: string
              nullable: true
            tag:
              type: string
              nullable: true
            modelo:
              type: string
              nullable: true
            username:
              type: string
              nullable: true
            partNumber:
              type: string
              nullable: true
            idFabricante:
              type: integer
              format: int32
            dataAquisicao:
              type: string
              format: date-time
            dataFimGarantia:
              type: string
              format: date-time
            data:
              type: string
              format: date-time
            idFornecedor:
              type: integer
              format: int32
            observacoes:
              type: string
              nullable: true
            estadoEquipamento: {}
            idLocalTrabalho:
              type: integer
              format: int32
            idFuncionario:
              type: integer
              format: int32
              nullable: true
            localizacaoArmazem:
              type: string
              nullable: true
            importGuid:
              type: string
              format: uuid
            etiquetaImpressaEm:
              type: string
              format: date-time
              nullable: true
            dataRetoma:
              type: string
              format: date-time
              nullable: true
            idImportacao:
              type: integer
              format: int32
            dadosGerfip: {}
            id:
              type: integer
              format: int32
            version:
              type: integer
              format: int32
        MsgAtualizacaoEquipamentoAtivo:
          type: object
          allOf:
            - $ref: '#/components/schemas/MsgAtualizacaoEquipamento'
            - properties:
                nome:
                  type: string
                  nullable: true
                numPortas:
                  type: integer
                  format: int32
                ip:
                  type: string
                  nullable: true
                velocidade:
                  type: string
                  nullable: true
                idTipoEquipamentoAtivo:
                  type: integer
                  format: int32
        MsgAtualizacaoEquipamentoComEquipamentosAssociados:
          required:
            - $type
          type: object
          properties:
            $type:
              type: string
            serialNumber:
              type: string
              nullable: true
            tag:
              type: string
              nullable: true
            modelo:
              type: string
              nullable: true
            username:
              type: string
              nullable: true
            partNumber:
              type: string
              nullable: true
            idFabricante:
              type: integer
              format: int32
            dataAquisicao:
              type: string
              format: date-time
            dataFimGarantia:
              type: string
              format: date-time
            data:
              type: string
              format: date-time
            idFornecedor:
              type: integer
              format: int32
            observacoes:
              type: string
              nullable: true
            estadoEquipamento: {}
            idLocalTrabalho:
              type: integer
              format: int32
            idFuncionario:
              type: integer
              format: int32
              nullable: true
            localizacaoArmazem:
              type: string
              nullable: true
            importGuid:
              type: string
              format: uuid
            etiquetaImpressaEm:
              type: string
              format: date-time
              nullable: true
            dataRetoma:
              type: string
              format: date-time
              nullable: true
            idImportacao:
              type: integer
              format: int32
            dadosGerfip: {}
            id:
              type: integer
              format: int32
            version:
              type: integer
              format: int32
            idsEquipamentosAssociar:
              type: array
              items: {}
              nullable: true
            idsEquipamentosCancelar:
              type: array
              items: {}
              nullable: true
            idsEquipamentosPropagar:
              type: array
              items: {}
              nullable: true
          discriminator:
            propertyName: $type
            mapping:
              Sra.Assistencias.Dtos.Equipamentos.MsgAtualizacaoComputador, Sra.Assistencias.Dtos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: '#/components/schemas/MsgAtualizacaoComputador'
              Sra.Assistencias.Dtos.Equipamentos.MsgAtualizacaoSoftware, Sra.Assistencias.Dtos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: '#/components/schemas/MsgAtualizacaoSoftware'
        MsgAtualizacaoEquipamentoGenerico:
          type: object
          allOf:
            - $ref: '#/components/schemas/MsgAtualizacaoEquipamento'
            - properties:
                idTipoEquipamentoGenerico:
                  type: integer
                  format: int32
        MsgAtualizacaoEquipamentos:
          type: object
          properties:
            observacoes:
              type: string
              nullable: true
            bytes:
              type: string
              format: byte
              nullable: true
        MsgAtualizacaoFuncionario:
          type: object
          properties:
            idFuncionario:
              type: integer
              format: int32
            version:
              type: integer
              format: int32
            nif:
              type: string
              nullable: true
            idLocalTrabalho:
              type: integer
              format: int32
            nome:
              type: string
              nullable: true
            carreira:
              type: string
              nullable: true
            numeroMecanografico:
              type: integer
              format: int32
            codUnidadeOrganica:
              type: integer
              format: int32
            situacaoProfissional: {}
            contactos:
              type: array
              items: {}
              nullable: true
            propagaLocalTrabalhoParaEquipamentos:
              type: boolean
          additionalProperties: false
        MsgAtualizacaoImpressora:
          type: object
          allOf:
            - $ref: '#/components/schemas/MsgAtualizacaoEquipamento'
            - properties:
                idTipoImpressora:
                  type: integer
                  format: int32
                ip:
                  type: string
                  nullable: true
                nomeNetBIOS:
                  type: string
                  nullable: true
        MsgAtualizacaoSoftware:
          type: object
          properties:
            numLicencasAdquiridas:
              type: integer
              format: int32
            nome:
              type: string
              nullable: true
            versao:
              type: string
              nullable: true
            upgrade:
              type: boolean
        MsgAtualizacaoComputador: {}
    
    

    Here's a sample of it working correctly

    redoc