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

[Question] optional fields in openapi body from dataclass vs validate #42

Closed
morph027 opened this issue Jan 12, 2022 · 3 comments · Fixed by #46
Closed

[Question] optional fields in openapi body from dataclass vs validate #42

morph027 opened this issue Jan 12, 2022 · 3 comments · Fixed by #46
Labels
bug Something isn't working

Comments

@morph027
Copy link

Description

Not sure if it's related to sanic-ext at all or just missing understanding of dataclasses on my side ;)

I'm trying to describe and validate a json body w/ optional parameters.

from dataclasses import dataclass
from typing import List, Optional
from sanic import Sanic
from sanic.response import json
from sanic_ext import openapi, validate


app = Sanic("Test")


@dataclass
class Test:
    foo: str
    bar: int
    foobar: Optional[List[str]]


@app.post("/")
@openapi.body({"application/json": Test})
@validate(Test)                                                                                                                                               
async def test(request, body: Test):
    return json({"message": "Hello world!"})

This renders as request body describing foobar as object or nullable.

Screenshot from 2022-01-12 09-50-17

Validation now fails when foobar is omitted (correct so far)

$ curl -d '{"foo": "string", "bar": 1337}' http://127.0.0.1:8000/
{"description":"Bad Request","status":400,"message":"Invalid request body: Test. Error: missing a required argument: 'foobar'"}

Now, if i set a default for foobar like

@dataclass
class Test:
    foo: str
    bar: int
    foobar: Optional[List[str]] = None

the request works as expected:

$ curl -d '{"foo": "string", "bar": 1337}' http://127.0.0.1:8000/
{"message":"Hello world!"}

But now API docs renders as object without nullable:

Screenshot from 2022-01-12 09-59-56

Question

How can i combine the two? Show foobar as nullable in docs and pass validation w/ parameter omitted?

Do i need 2 seperate dataclasses?

Also, is it possible to show foobar as list of strings in the docs instead of object (derives from Optional, i guess).

Versions

  • sanic 21.12.0
  • sanic-ext 21.12.3
@ahopkins ahopkins added the bug Something isn't working label Jan 15, 2022
@ahopkins
Copy link
Member

ahopkins commented Jan 15, 2022

I think it ultimately is a bug in need of fixing to properly read more information from the dataclass if it is available.

@morph027
Copy link
Author

🥳 It works, thank you!

I do have another corner case where a request does have only one optional parameter.

Example:

from dataclasses import dataclass, field
from typing import Optional
from sanic import Sanic
from sanic.log import logger
from sanic.response import json
from sanic_ext import openapi, validate


app = Sanic("Test")


@dataclass
class Test:
    bar: Optional[str] = field(default_factory=str)


@app.post("/")
@openapi.body({"application/json": Test})
@validate(Test)
async def test(request, body: Test):
    return json({"message": "Hello world!"})

If i omit the body completely, the validation fails again due to the request does not have a body (None):

$ curl -X POST http://127.0.0.1:8000/
{"description":"Bad Request","status":400,"message":"Invalid request body: Test. Error: Value 'None' is not a dict"}

Stacktrace:

[2022-01-21 13:33:53 +0100] [30740] [ERROR] Exception occurred while handling uri: 'http://127.0.0.1:8000/'
Traceback (most recent call last):
  File "/home/morph/python3/lib/python3.10/site-packages/sanic_ext/extras/validation/validators.py", line 24, in validate_body
    return validator(model, body)
  File "/home/morph/python3/lib/python3.10/site-packages/sanic_ext/extras/validation/validators.py", line 36, in _validate_annotations
    return check_data(model, body, schema, allow_multiple, allow_coerce)
  File "/home/morph/python3/lib/python3.10/site-packages/sanic_ext/extras/validation/check.py", line 83, in check_data
    raise TypeError(f"Value '{data}' is not a dict")
TypeError: Value 'None' is not a dict

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "handle_request", line 83, in handle_request
    )
  File "/home/morph/python3/lib/python3.10/site-packages/sanic_ext/extensions/openapi/openapi.py", line 213, in handler
    retval = await retval
  File "/home/morph/python3/lib/python3.10/site-packages/sanic_ext/extras/validation/decorator.py", line 37, in decorated_function
    await do_validation(
  File "/home/morph/python3/lib/python3.10/site-packages/sanic_ext/extras/validation/setup.py", line 41, in do_validation
    validation = validate_body(validator, model, data)
  File "/home/morph/python3/lib/python3.10/site-packages/sanic_ext/extras/validation/validators.py", line 26, in validate_body
    raise ValidationError(
sanic_ext.exceptions.ValidationError: Invalid request body: Test. Error: Value 'None' is not a dict

Passing an empty body works:

$ curl -X POST -d '{}' http://127.0.0.1:8000/
{"message":"Hello world!"}

Any way to specify a fallback for a None body?

@morph027
Copy link
Author

Ah, nevermind, read through the docs again and found a convenient way to create a dummy body using a middleware.

from dataclasses import dataclass, field
from typing import Optional

from sanic import Sanic
from sanic.response import json
from sanic_ext import openapi, validate

app = Sanic("Test")


@dataclass
class Test:
    bar: Optional[str] = field(default_factory=str)


@app.middleware("request")
async def dummy_body_if_empty(request):
    if not request.body:
        request.body = b'{}'


@app.post("/")
@openapi.body({"application/json": Test})
@validate(Test)
async def test(request, body: Test):
    return json({"message": "Hello world!"})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants