drf-standardized-errors icon indicating copy to clipboard operation
drf-standardized-errors copied to clipboard

Use Pattern type rather than Enum type for error component attr property

Open GiancarloFusiello opened this issue 1 year ago • 2 comments

As described here in issue #74 , for DRF serializer ListField or DictField drf-spectacular combined with drf-standardized-errors will generate the following:

list_field_nameINDEXErrorComponent:
      type: object
      properties:
        attr:
          enum:
          - list_field_name.INDEX
          type: string
          description: '* `list_field_name.INDEX` - list_field_name.INDEX'
        code:
          enum:
          - blank
          - invalid
          - max_length
          - 'null'
          - null_characters_not_allowed
          - required
          - surrogate_characters_not_allowed
          type: string
          description: |-
            * `blank` - blank
            * `invalid` - invalid
            * `max_length` - max_length
            * `null` - null
            * `null_characters_not_allowed` - null_characters_not_allowed
            * `required` - required
            * `surrogate_characters_not_allowed` - surrogate_characters_not_allowed
        detail:
          type: string

If you're like me and use Schemathesis to generate and run tests against your documented API, this will result in errors due to list_field_name.INDEX being a string literal as see here:

- Response violates schema

    'null' is not one of ['parse_error']

    Schema:

        {
            "enum": [
                "parse_error"
            ],
            "type": "string",
            "description": "* `parse_error` - Parse Error"
        }

    Value:

        "null"

[400] Bad Request:

    `{"type":"validation_error","errors":[{"code":"null","detail":"This field may not be null.","attr":"list_field_name.0"}]}`

Reproduce with:

    curl -X POST -H 'Authorization: [Filtered]' -H 'Content-Type: application/json' -H 'Cookie: [Filtered]' -d '{"list_field_name": [null]}' http://0.0.0.0:8000/api/my-resource/

Suggestion To use the Pattern data type rather than Enum for the attr error component property. Examples can be found here.

GiancarloFusiello avatar Jun 20 '24 16:06 GiancarloFusiello

Sorry it took me so long to get to this and unfortunately, I don't have good news. The gist of it is: using a pattern for attr isn't enough to solve the schema conformance issues raised by schemathesis. For now, I'll keep things as they are until we have a solution for all issues raised by schemathesis.

in #79, I went ahead and added schemathesis to the test suite along with the exact cases reproducing the issues described here. After that, I updated the implementation so that attr is a string with a pattern (for a list field, the pattern is some_list_field\.\d+). Still, schemathesis kept showing similar conformance issues (can be seen in the PR checks). Looking at the docs, I thought this FAQ entry will help since drf-spectacular doesn't add additionalProperties: false by default. So, I tried adding that manually to see if it solves the issue. Again, schemathesis kept raising a conformance issue.

Then I created a minimal API spec to experiment with it (notice how attr is set as a string with a pattern)

API Spec

openapi: 3.0.3
info:
  title: API
  version: 1.0.0
  description: Amazing API
paths:
  /fuzzing/list_field/:
    post:
      operationId: fuzzing_list_field_create
      tags:
        - fuzzing
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ListFieldFuzzing'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListFieldFuzzing'
          description: ''
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FuzzingListFieldCreateErrorResponse400'
          description: ''
components:
  schemas:
    FuzzingListFieldCreateError:
      oneOf:
        - $ref: '#/components/schemas/FuzzingListFieldCreateNonFieldErrorsErrorComponent'
        - $ref: '#/components/schemas/FuzzingListFieldCreateField1ErrorComponent'
        - $ref: '#/components/schemas/FuzzingListFieldCreateField1INDEXErrorComponent'
      discriminator:
        propertyName: attr
        mapping:
          non_field_errors: '#/components/schemas/FuzzingListFieldCreateNonFieldErrorsErrorComponent'
          field1: '#/components/schemas/FuzzingListFieldCreateField1ErrorComponent'
          field1.INDEX: '#/components/schemas/FuzzingListFieldCreateField1INDEXErrorComponent'
    FuzzingListFieldCreateErrorResponse400:
      oneOf:
        - $ref: '#/components/schemas/FuzzingListFieldCreateValidationError'
      discriminator:
        propertyName: type
        mapping:
          validation_error: '#/components/schemas/FuzzingListFieldCreateValidationError'
    FuzzingListFieldCreateField1ErrorComponent:
      type: object
      properties:
        attr:
          type: string
          pattern: field1
        code:
          enum:
            - not_a_list
            - 'null'
            - required
          type: string
        detail:
          type: string
      required:
        - attr
        - code
        - detail
      additionalProperties: false
    FuzzingListFieldCreateField1INDEXErrorComponent:
      type: object
      properties:
        attr:
          type: string
          pattern: field1\.\d+
        code:
          enum:
            - invalid
            - max_string_length
            - 'null'
            - required
          type: string
        detail:
          type: string
      required:
        - attr
        - code
        - detail
      additionalProperties: false
    FuzzingListFieldCreateNonFieldErrorsErrorComponent:
      type: object
      properties:
        attr:
          type: string
          pattern: non_field_errors
        code:
          enum:
            - invalid
            - 'null'
          type: string
        detail:
          type: string
      required:
        - attr
        - code
        - detail
      additionalProperties: false
    FuzzingListFieldCreateValidationError:
      type: object
      properties:
        type:
          $ref: '#/components/schemas/ValidationErrorEnum'
        errors:
          type: array
          items:
            $ref: '#/components/schemas/FuzzingListFieldCreateError'
      required:
        - errors
        - type
      additionalProperties: false
    ListFieldFuzzing:
      type: object
      properties:
        field1:
          type: array
          items:
            type: integer
      required:
        - field1
      additionalProperties: false
    ValidationErrorEnum:
      enum:
        - validation_error
      type: string

The above API spec generates the following conformance issue

Conformance issue details

E       1. Response violates schema
E       
E           {'code': 'null', 'detail': 'This field may not be null.', 'attr': 'field1.0'} is valid under each of {'$ref': '#/components/schemas/FuzzingListFieldCreateField1INDEXErrorComponent'}, {'$ref': '#/components/schemas/FuzzingListFieldCreateField1ErrorComponent'}
E       
E           Schema at /oneOf/0/properties/errors/items:
E       
E               {
E                   "oneOf": [
E                       {
E                           "$ref": "#/components/schemas/FuzzingListFieldCreateNonFieldError...
E                       },
E                       {
E                           "$ref": "#/components/schemas/FuzzingListFieldCreateField1ErrorCo...
E                       },
E                       {
E                           "$ref": "#/components/schemas/FuzzingListFieldCreateField1INDEXEr...
E                       }
E                   ],
E                   "discriminator": {
E                       "propertyName": "attr",
E                       "mapping": {
E                           "non_field_errors": "#/components/schemas/FuzzingListFieldCreateN...
E                           "field1": "#/components/schemas/FuzzingListFieldCreateField1Error...
E                           "field1.INDEX": "#/components/schemas/FuzzingListFieldCreateField...
E                       }
E                   // Output truncated...
E               }
E       
E           Value:
E       
E               {
E                   "code": "null",
E                   "detail": "This field may not be null.",
E                   "attr": "field1.0"
E               }
E       
E       [400] Bad Request:
E       
E           `{"type":"validation_error","errors":[{"code":"null","detail":"This field may not be null.","attr":"field1.0"}]}`
E       
E       Reproduce with: 
E       
E           curl -X POST -H 'Content-Type: application/json' -d '{"field1": [null]}' http://127.0.0.1:8000/fuzzing/list_field/
E       
E       Falsifying explicit example: test_compliance_to_api_schema_for_list_field(
E           case=Case(body={'field1': [None]}),
E       )

That's when I understood the real issue is in the discriminator mapping which still shows field1.INDEX:

    FuzzingListFieldCreateError:
      oneOf:
        - $ref: '#/components/schemas/FuzzingListFieldCreateNonFieldErrorsErrorComponent'
        - $ref: '#/components/schemas/FuzzingListFieldCreateField1ErrorComponent'
        - $ref: '#/components/schemas/FuzzingListFieldCreateField1INDEXErrorComponent'
      discriminator:
        propertyName: attr
        mapping:
          non_field_errors: '#/components/schemas/FuzzingListFieldCreateNonFieldErrorsErrorComponent'
          field1: '#/components/schemas/FuzzingListFieldCreateField1ErrorComponent'
 ======>  field1.INDEX: '#/components/schemas/FuzzingListFieldCreateField1INDEXErrorComponent'

Afterwards, I tried to see if it's possible to express the pattern at the discriminator level in openapi but couldn't find a way to do that. That led me to think: even though #79 is a step in the right direction, the API spec generated doesn't properly describe the API response. So for now, I'll keep things as they are without merging #79 until openapi adds some sort of conditional schemas that fully solves the issue or someone finds a different solution.

ghazi-git avatar Jul 26 '24 21:07 ghazi-git

I see. That's a shame. Thanks for trying @ghazi-git.

GiancarloFusiello avatar Jul 27 '24 14:07 GiancarloFusiello