Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

how to represent a query parameter of type array? #267

Closed
ricardogsilva opened this issue Jan 20, 2021 · 9 comments
Closed

how to represent a query parameter of type array? #267

ricardogsilva opened this issue Jan 20, 2021 · 9 comments
Labels
enhancement New feature or request fix confirmation pending issue has been fixed and confirmation from issue reporter is pending

Comments

@ricardogsilva
Copy link

I'm working on an implementation of OGC API Features

I need to implement the bbox query parameter, whose schema is shown in section 7.15.3. It is modeled as an array and is further constrained by minItems and maxItems.

Is there a way to achieve such a schema definition with drf-spectacular? Or even a way to manually override the schema just for a specific query param?

@tfranzel
Copy link
Owner

tfranzel commented Jan 21, 2021

hi @ricardogsilva, that feature was only partially exposed. i added the missing part to the external interface.

you need to decorate the method with extend schema (see the doc for further instructions). type can be various things including a raw schema dict.

    @extend_schema(
        parameters=[OpenApiParameter(
            name='bbox',
            type={'type': 'array', 'minItems': 4, 'maxItems': 6, 'items': {'type': 'number'}},
            location=OpenApiParameter.QUERY,
            required=False,
            style='form',
            explode=False,
        )],
    )
    def list(self, request):
        ...

EDIT: For people wanting to use array parameters, we added a many parameter to OpenApiParameter that makes this less cumbersome in case you do not need minItems/maxItems. This feature is available in more recent versions of spectacular.

@tfranzel tfranzel added fix confirmation pending issue has been fixed and confirmation from issue reporter is pending enhancement New feature or request labels Jan 21, 2021
@ricardogsilva
Copy link
Author

awesome, I'll test it out soon and provide some feedback!

@leigh-johnson
Copy link

I gave this a whirl, but the request body types are being rendered with non-repeating object field.

I'm trying to produce a schema where the mask field (model below) is an array. Apologies if I'm missing additional steps required to render these in the body types.

I love this library, by the way! Thank you for all of your hard work. ❤️

Version Info

django==3.1.3 
djangorestframework==3.12.2
drf-spectacular==0.13.2

Generated Request Types

            "DeviceCalibration": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "integer",
                        "readOnly": true
                    },
                    "created_dt": {
                        "type": "string",
                        "format": "date-time",
                        "readOnly": true
                    },
                    "updated_dt": {
                        "type": "string",
                        "format": "date-time",
                        "readOnly": true
                    },
                    "octoprint_device": {
                        "type": "integer"
                    },
                    "fpm": {
                        "type": "integer",
                        "maximum": 2147483647,
                        "minimum": -2147483648,
                        "nullable": true
                    },
                    "coordinates": {
                        "type": "object",
                        "additionalProperties": {},
                        "nullable": true
                    },
                    "mask": {
                        "type": "object",
                        "additionalProperties": {},
                        "nullable": true
                    },
                    "url": {
                        "type": "string",
                        "format": "uri",
                        "readOnly": true
                    }
                },
                "required": [
                    "octoprint_device"
                ]
            },
            "DeviceCalibrationRequest": {
                "type": "object",
                "properties": {
                    "octoprint_device": {
                        "type": "integer"
                    },
                    "fpm": {
                        "type": "integer",
                        "maximum": 2147483647,
                        "minimum": -2147483648,
                        "nullable": true
                    },
                    "coordinates": {
                        "type": "object",
                        "additionalProperties": {},
                        "nullable": true
                    },
                    "mask": {
                        "type": "object",
                        "additionalProperties": {},
                        "nullable": true
                    }
                },
                "required": [
                    "octoprint_device"
                ]
            }

Generated API endpoints

These are looking good to me! 👍 Included just for reference / debugging purposes.

        "/api/device-calibrations/": {
            "get": {
                "operationId": "device_calibrations_list",
                "description": "",
                "parameters": [
                    {
                        "in": "query",
                        "name": "mask",
                        "schema": {
                            "type": "array",
                            "items": {
                                "type": "number"
                            }
                        },
                        "explode": false
                    },
                    {
                        "name": "page",
                        "required": false,
                        "in": "query",
                        "description": "A page number within the paginated result set.",
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "tags": [
                    "ml-ops"
                ],
                "security": [
                    {
                        "cookieAuth": []
                    },
                    {
                        "tokenAuth": []
                    }
                ],
                "responses": {
                    "200": {
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PaginatedDeviceCalibrationList"
                                }
                            }
                        },
                        "description": ""
                    }
                }
            }
        },
        "/api/device-calibrations/{id}/": {
            "get": {
                "operationId": "device_calibrations_retrieve",
                "description": "",
                "parameters": [
                    {
                        "in": "path",
                        "name": "id",
                        "schema": {
                            "type": "integer"
                        },
                        "description": "A unique integer value identifying this device calibration.",
                        "required": true
                    },
                    {
                        "in": "query",
                        "name": "mask",
                        "schema": {
                            "type": "array",
                            "items": {
                                "type": "number"
                            }
                        },
                        "explode": false
                    }
                ],
                "tags": [
                    "ml-ops"
                ],
                "security": [
                    {
                        "cookieAuth": []
                    },
                    {
                        "tokenAuth": []
                    }
                ],
                "responses": {
                    "200": {
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/DeviceCalibration"
                                }
                            }
                        },
                        "description": ""
                    }
                }
            },
            "put": {
                "operationId": "device_calibrations_update",
                "description": "",
                "parameters": [
                    {
                        "in": "path",
                        "name": "id",
                        "schema": {
                            "type": "integer"
                        },
                        "description": "A unique integer value identifying this device calibration.",
                        "required": true
                    },
                    {
                        "in": "query",
                        "name": "mask",
                        "schema": {
                            "type": "array",
                            "items": {
                                "type": "number"
                            }
                        },
                        "explode": false
                    }
                ],
                "tags": [
                    "ml-ops"
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/DeviceCalibrationRequest"
                            }
                        },
                        "application/x-www-form-urlencoded": {
                            "schema": {
                                "$ref": "#/components/schemas/DeviceCalibrationRequest"
                            }
                        },
                        "multipart/form-data": {
                            "schema": {
                                "$ref": "#/components/schemas/DeviceCalibrationRequest"
                            }
                        }
                    },
                    "required": true
                },
                "security": [
                    {
                        "cookieAuth": []
                    },
                    {
                        "tokenAuth": []
                    }
                ],
                "responses": {
                    "200": {
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/DeviceCalibration"
                                }
                            }
                        },
                        "description": ""
                    }
                }
            },
            "patch": {
                "operationId": "device_calibrations_partial_update",
                "description": "",
                "parameters": [
                    {
                        "in": "path",
                        "name": "id",
                        "schema": {
                            "type": "integer"
                        },
                        "description": "A unique integer value identifying this device calibration.",
                        "required": true
                    },
                    {
                        "in": "query",
                        "name": "mask",
                        "schema": {
                            "type": "array",
                            "items": {
                                "type": "number"
                            }
                        },
                        "explode": false
                    }
                ],
                "tags": [
                    "ml-ops"
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/PatchedDeviceCalibrationRequest"
                            }
                        },
                        "application/x-www-form-urlencoded": {
                            "schema": {
                                "$ref": "#/components/schemas/PatchedDeviceCalibrationRequest"
                            }
                        },
                        "multipart/form-data": {
                            "schema": {
                                "$ref": "#/components/schemas/PatchedDeviceCalibrationRequest"
                            }
                        }
                    }
                },
                "security": [
                    {
                        "cookieAuth": []
                    },
                    {
                        "tokenAuth": []
                    }
                ],
                "responses": {
                    "200": {
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/DeviceCalibration"
                                }
                            }
                        },
                        "description": ""
                    }
                }
            }
        },
        "/api/device-calibrations/update-or-create/": {
            "post": {
                "operationId": "device_calibration_update_or_create",
                "description": "",
                "parameters": [
                    {
                        "in": "query",
                        "name": "mask",
                        "schema": {
                            "type": "array",
                            "items": {
                                "type": "number"
                            }
                        },
                        "required": true,
                        "explode": false
                    }
                ],
                "tags": [
                    "ml-ops"
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/DeviceCalibrationRequest"
                            }
                        },
                        "application/x-www-form-urlencoded": {
                            "schema": {
                                "$ref": "#/components/schemas/DeviceCalibrationRequest"
                            }
                        },
                        "multipart/form-data": {
                            "schema": {
                                "$ref": "#/components/schemas/DeviceCalibrationRequest"
                            }
                        }
                    },
                    "required": true
                },
                "security": [
                    {
                        "cookieAuth": []
                    },
                    {
                        "tokenAuth": []
                    }
                ],
                "responses": {
                    "200": {
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/DeviceCalibration"
                                }
                            }
                        },
                        "description": ""
                    }
                }
            }
        },

Viewset

# views.py
@extend_schema(
    tags=["ml-ops"],
    parameters=[OpenApiParameter(
        name='mask',
        type={'type': 'array', 'items': {'type': 'number'}},
        location=OpenApiParameter.QUERY,
        explode=False
    )]
)
class DeviceCalibrationViewSet(
    UpdateModelMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet
):
    serializer_class = DeviceCalibrationSerializer
    queryset = DeviceCalibration.objects.all()
    lookup_field = "id"

    def get_queryset(self):
        user = self.request.user
        return DeviceCalibration.objects.filter(octoprint_device__user=user).all()

    @extend_schema(
        operation_id="device_calibration_update_or_create",
        parameters=[OpenApiParameter(
            name='mask',
            type={'type': 'array', 'items': {'type': 'number'}},
            location=OpenApiParameter.QUERY,
            required=True,
            explode=False
        )]
    )
    @action(methods=["post"], detail=False, url_path="update-or-create")
    def update_or_create(self, request):

        serializer = self.get_serializer(data=request.data)
        if serializer.is_valid():
            instance, created = serializer.update_or_create(serializer.validated_data)
            response_serializer = self.get_serializer(instance)
            if not created:
                return Response(response_serializer.data, status=status.HTTP_202_ACCEPTED)
            return Response(response_serializer.data, status=status.HTTP_201_CREATED)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def perform_create(self, serializer):
        serializer.save(user=self.request.user)
# models.py
class DeviceCalibration(models.Model):
    created_dt = models.fields.DateTimeField(auto_now_add=True)
    updated_dt = models.fields.DateTimeField(auto_now=True)
    octoprint_device = models.OneToOneField(
        "remote_control.OctoPrintDevice", on_delete=models.CASCADE
    )
    fpm = models.IntegerField(null=True)
    coordinates = JSONField(null=True)
    mask = JSONField(null=True)

@leigh-johnson
Copy link

Ah, of course I figure out how to do this 10 minutes after taking a sec to ask. 😂

For anyone else who stumbles into this issue, here are the modifications I made (compared to the code in my previous post). I've also included the outputs below.

Serializer

extend_schema_field did the trick for me. =)

# serializers.py
@extend_schema_field(field={'type': 'array', 'items': {'type': 'number'}})
class JSONArrayField(serializers.JSONField):
    pass

class DeviceCalibrationSerializer(serializers.ModelSerializer):

    mask = JSONArrayField()

    class Meta:
        model = DeviceCalibration
        fields = [field.name for field in DeviceCalibration._meta.fields] + [
            "url",
        ]
        extra_kwargs = {
            "url": {"view_name": "api:device-calibration-detail", "lookup_field": "id"},
        }

    def update_or_create(self, validated_data):
        return DeviceCalibration.objects.update_or_create(defaults=validated_data)

Viewset

# views.py
@extend_schema(
    tags=["ml-ops"]
)
class DeviceCalibrationViewSet(
    UpdateModelMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet
):
    serializer_class = DeviceCalibrationSerializer
    queryset = DeviceCalibration.objects.all()
    lookup_field = "id"

    def get_queryset(self):
        user = self.request.user
        return DeviceCalibration.objects.filter(octoprint_device__user=user).all()

    @extend_schema(
        operation_id="device_calibration_update_or_create"
    )
    @action(methods=["post"], detail=False, url_path="update-or-create")
    def update_or_create(self, request):

        serializer = self.get_serializer(data=request.data)
        if serializer.is_valid():
            instance, created = serializer.update_or_create(serializer.validated_data)
            response_serializer = self.get_serializer(instance)
            if not created:
                return Response(response_serializer.data, status=status.HTTP_202_ACCEPTED)
            return Response(response_serializer.data, status=status.HTTP_201_CREATED)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def perform_create(self, serializer):
        serializer.save(user=self.request.user)

Outputs

🎉 Array data structures in the request/response bodies and endpoint schemas. Woo-hoo!

        "/api/device-calibrations/": {
            "get": {
                "operationId": "device_calibrations_list",
                "description": "",
                "parameters": [
                    {
                        "name": "page",
                        "required": false,
                        "in": "query",
                        "description": "A page number within the paginated result set.",
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "tags": [
                    "ml-ops"
                ],
                "security": [
                    {
                        "cookieAuth": []
                    },
                    {
                        "tokenAuth": []
                    }
                ],
                "responses": {
                    "200": {
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PaginatedDeviceCalibrationList"
                                }
                            }
                        },
                        "description": ""
                    }
                }
            }
        },
        "/api/device-calibrations/{id}/": {
            "get": {
                "operationId": "device_calibrations_retrieve",
                "description": "",
                "parameters": [
                    {
                        "in": "path",
                        "name": "id",
                        "schema": {
                            "type": "integer"
                        },
                        "description": "A unique integer value identifying this device calibration.",
                        "required": true
                    }
                ],
                "tags": [
                    "ml-ops"
                ],
                "security": [
                    {
                        "cookieAuth": []
                    },
                    {
                        "tokenAuth": []
                    }
                ],
                "responses": {
                    "200": {
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/DeviceCalibration"
                                }
                            }
                        },
                        "description": ""
                    }
                }
            },
            "put": {
                "operationId": "device_calibrations_update",
                "description": "",
                "parameters": [
                    {
                        "in": "path",
                        "name": "id",
                        "schema": {
                            "type": "integer"
                        },
                        "description": "A unique integer value identifying this device calibration.",
                        "required": true
                    }
                ],
                "tags": [
                    "ml-ops"
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/DeviceCalibrationRequest"
                            }
                        },
                        "application/x-www-form-urlencoded": {
                            "schema": {
                                "$ref": "#/components/schemas/DeviceCalibrationRequest"
                            }
                        },
                        "multipart/form-data": {
                            "schema": {
                                "$ref": "#/components/schemas/DeviceCalibrationRequest"
                            }
                        }
                    },
                    "required": true
                },
                "security": [
                    {
                        "cookieAuth": []
                    },
                    {
                        "tokenAuth": []
                    }
                ],
                "responses": {
                    "200": {
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/DeviceCalibration"
                                }
                            }
                        },
                        "description": ""
                    }
                }
            },
            "patch": {
                "operationId": "device_calibrations_partial_update",
                "description": "",
                "parameters": [
                    {
                        "in": "path",
                        "name": "id",
                        "schema": {
                            "type": "integer"
                        },
                        "description": "A unique integer value identifying this device calibration.",
                        "required": true
                    }
                ],
                "tags": [
                    "ml-ops"
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/PatchedDeviceCalibrationRequest"
                            }
                        },
                        "application/x-www-form-urlencoded": {
                            "schema": {
                                "$ref": "#/components/schemas/PatchedDeviceCalibrationRequest"
                            }
                        },
                        "multipart/form-data": {
                            "schema": {
                                "$ref": "#/components/schemas/PatchedDeviceCalibrationRequest"
                            }
                        }
                    }
                },
                "security": [
                    {
                        "cookieAuth": []
                    },
                    {
                        "tokenAuth": []
                    }
                ],
                "responses": {
                    "200": {
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/DeviceCalibration"
                                }
                            }
                        },
                        "description": ""
                    }
                }
            }
        },
        "/api/device-calibrations/update-or-create/": {
            "post": {
                "operationId": "device_calibration_update_or_create",
                "description": "",
                "tags": [
                    "ml-ops"
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/DeviceCalibrationRequest"
                            }
                        },
                        "application/x-www-form-urlencoded": {
                            "schema": {
                                "$ref": "#/components/schemas/DeviceCalibrationRequest"
                            }
                        },
                        "multipart/form-data": {
                            "schema": {
                                "$ref": "#/components/schemas/DeviceCalibrationRequest"
                            }
                        }
                    },
                    "required": true
                },
                "security": [
                    {
                        "cookieAuth": []
                    },
                    {
                        "tokenAuth": []
                    }
                ],
                "responses": {
                    "200": {
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/DeviceCalibration"
                                }
                            }
                        },
                        "description": ""
                    }
                }
            }
        },
            "DeviceCalibration": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "integer",
                        "readOnly": true
                    },
                    "created_dt": {
                        "type": "string",
                        "format": "date-time",
                        "readOnly": true
                    },
                    "updated_dt": {
                        "type": "string",
                        "format": "date-time",
                        "readOnly": true
                    },
                    "octoprint_device": {
                        "type": "integer"
                    },
                    "fpm": {
                        "type": "integer",
                        "maximum": 2147483647,
                        "minimum": -2147483648,
                        "nullable": true
                    },
                    "coordinates": {
                        "type": "object",
                        "additionalProperties": {},
                        "nullable": true
                    },
                    "mask": {
                        "type": "array",
                        "items": {
                            "type": "number"
                        }
                    },
                    "url": {
                        "type": "string",
                        "format": "uri",
                        "readOnly": true
                    }
                },
                "required": [
                    "mask",
                    "octoprint_device"
                ]
            },
            "DeviceCalibrationRequest": {
                "type": "object",
                "properties": {
                    "octoprint_device": {
                        "type": "integer"
                    },
                    "fpm": {
                        "type": "integer",
                        "maximum": 2147483647,
                        "minimum": -2147483648,
                        "nullable": true
                    },
                    "coordinates": {
                        "type": "object",
                        "additionalProperties": {},
                        "nullable": true
                    },
                    "mask": {
                        "type": "array",
                        "items": {
                            "type": "number"
                        }
                    }
                },
                "required": [
                    "mask",
                    "octoprint_device"
                ]
            },

@tfranzel
Copy link
Owner

@leigh-johnson haha, thank you very much. 😄

as you figured this issue was about having arrays in the query parameters (query,header,cookie,...), which is a different issue from yours.

looking at your solution, its probably as elegant as it gets and not much room for improvement. as a matter of fact i recently added the extend_schema_field raw dict feature for me but have not gotten around using it yet. so you used it first 😆

@tfranzel
Copy link
Owner

@ricardogsilva i'll close this issue as i'm quite confident that your problem is solved. if not feel free to comment and we'll revisit.

@ricardogsilva
Copy link
Author

@tfranzel

Yes, go ahead, I meant having this tested out already, but life has gotten in the way 😸

If needed I will provide futher detail when I get back to working on this. Thanks a lot!

@fidoriel
Copy link

Is there a way to make explode=False the system wide default?

@tfranzel
Copy link
Owner

@fidoriel nope and there likely won't be. We do set it automatically where appropriate. Otherwise, it is omitted from the schema, in which case default handling applies. Or, of course, you apply it by hand.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request fix confirmation pending issue has been fixed and confirmation from issue reporter is pending
Projects
None yet
Development

No branches or pull requests

4 participants