-
-
Notifications
You must be signed in to change notification settings - Fork 345
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
Docs: Explain _how_ dataclass & TypedDict are supported as (body) models #3202
Comments
The "how" of the support for the various types is kind of implementation details which is why I assume it hasn't been documented. Basically all the validation etc. comes from The reason pydantic is kept in that list even though it's not part of the standard library, it's very widely used and Pydantic was a core part of starlite (v1). NOTE: These are all assumptions I'm making because I wasn't a part of the project while those docs were written. |
@dataclass
class MyBody:
foo1: Annotated[str, field(default="dummy"), Parameter(description="ipsum", gt=1)]
foo2: Annotated[str, Parameter(description="ipsum", gt=1)] = "dummy"
foo3: str = "dummy"
Specifying the default like in |
Hmm, I don't get this. Can you re-phrase the same 😄 ? |
It is, but not within from typing import Annotated
from dataclasses import dataclass, field, fields
@dataclass
class One:
some: str = field(default="thing")
@dataclass
class Two:
some: Annotated[str, field(default="thing")]
print(fields(One)[0].default) # "something"
print(fields(Two)[0].default) # <dataclasses._MISSING_TYPE object at 0x7f297be13290> In general, Litestar doesn't try to enhance the support of the modelling library you chose beyond what it offers out of the box. If you want the Pydantic feature set, you should use Pydantic. If you want the attrs feature set, use attrs, etc. The reason for keeping it this way is that Litestar isn't trying to be a modelling library itself, and even though we already have quite a lot of code adjacent to these topics, I think it's important we stick to this rule so as to not let things get too bloated and complex.
Is that important? It might be interesting to some people, from a technical perspective, but in general I would consider this an implementation detail.
True, but that's because it is enabled by default, so the fact that support is implemented via a plugin is also just an implementation detail. Although, now that we've added some configuration options to this plugin, we might want to reconsider this stance. |
Thanks for the clarifications. I wasn't aware that Despite those facts, it's not obvious to the reader where the limitations are. At least my expectation was that you could use TypedDicts and dataclasses at least to the basic level (ie. generating OpenAPI constraints). Maybe even add custom validations, etc. But if you cannot do those, it should be good to highlight that. "Support is TypedDicts and dataclasses is very simple, and you cannot ...". Also, what good is supporting them in the first place, if you cannot do even the simplest of customization with them (e.g. setting field description)? Do people actually use them...? In Discord
No, I havent' tried directly that. I did try I believe the validity of this ticket stands - this topic should be not only better instructed, but someone should decide what is actually supported and how (and then briefly document it). |
My issue here is that documenting what is not supported is quite tricky and it's easier to just assume only the things that are documented are supported.
I'd say mostly backwards compatibility and users who already happen to have those types at hand, e.g. from a third party library or other pre-existing code. It would also require an active effort from our part to not support
Could you elaborate why that was your expectation? Constraints and custom validations are something neither
Expanding on this, personally, I would find it confusing if I read docs that said "You cannot use value constraints in TypedDicts" because I would immediately think "I must have missed something, since when do TypedDicts support value constraints?". It seems counterintuitive to me to point out that a feature of a thing isn't supported if that feature isn't actually a feature of that thing in the first place. |
Also
Yes, that's him :) |
I think that would (should) be considered a hack and is definitely not an intentional public API for this 👀 |
So people use those? Maybe in some super simple internal endpoints, I guess.
It's what you expect of a web framework - that you can validate fields and customize the generated OpenAPI (one of the selling points of Litestar/FastAPI). It's not a technical judgement, that when I see a
From my perspective, it's a good note. It helps me to realize that "ahh okay so dataclasses/TypedDicts are just for some simpe poccing and not for actual use". |
I don't know if it's restricted to that, but even then, it's worth supporting :)
This part I don't understand. They are very much for actual use, it's more like "If I need features that dataclasses/TypedDicts don't have, I need to use something other than dataclasses/TypedDicts". That's why Litestar has adopted the "bring your own modelling library" approach. If you want the Pydantic features, use Pydantic. If you want the msgspec features, use msgspec. If you want the (c)attrs feature, use (c)attrs. If you want to use another library that's not supported out of the box, you can write your own plugin. And if you need more feature than TypedDicts have, than don't use TypedDicts. If we were going to be strict about what you're asking, we'd have to list every difference in modelling behaviour between all those libraries. I wouldn't expect a Pydantic constraint to work on an attrs class. Why should that be different for dataclasses/TypedDicts? I'd like to make it obvious from reading the docs that for each library, you get the feature set that it provides. |
I understand your argument, but I think mine makes sense as well. My argument is that you should hint the reader of what to expect, and your view differs. Maybe it's not worth iterating this more than this, and we can let other chime in if they will. As I see it, this topic has been a bit confusing also for the other Litestar maintainers so I think there's a case here. But if you think there's nothing to do here, feel free to close it. Even having a ticket about it will help people in the future to find the reasoning and history, if they're wondering about the same. |
I find this a little confusing:
It is in contrast to the reality of what we actually do. We pull anything out of the metadata for a type that we parse and add it to a Lines 104 to 130 in 6e7e54a
Yet we have a unit test checking that litestar/tests/unit/test_openapi/test_schema.py Lines 300 to 329 in 6e7e54a
So for the following application: from dataclasses import dataclass
import annotated_types
from typing_extensions import Annotated
from litestar import Litestar, post
@dataclass
class Foo:
foo: Annotated[str, annotated_types.MaxLen(3)]
@post()
async def post_handler(data: Foo) -> None:
...
app = Litestar([post_handler]) We do render the constraint: .. yet it is not enforced: However, add a DTO into the mix: @post(dto=DataclassDTO[Foo])
async def post_handler(data: Foo) -> None:
... And now those constraints are enforced: This is all a by-product of the way that we indiscriminately parse anything out of type metadata if it smells like a thing that declares schema constraints, immediately upon parsing the type. And once we have the constraint parsed, the DTOs will convert it into It also opens us up to weird issues like this app failing on schema generation: from dataclasses import dataclass
from typing_extensions import Annotated
from litestar import Litestar, post
@dataclass
class Foo:
foo: Annotated[str, "totally unrelated metadata"]
@post()
async def post_handler(data: Foo) -> None:
...
app = Litestar([post_handler], debug=True) This occurs because the string
We make no attempt to ignore metadata that doesn't apply to us.
This is the way it should be. Metadata should only be parsed by plugins that are aware of the framework they represent and how that framework carries constraint info, if at all. The current approach of indiscriminate metadata parsing opens us up to a world of hyrums-law hurt. E.g., this works: from dataclasses import dataclass
from typing_extensions import Annotated
from litestar import Litestar, post
class Constraint:
max_length = 3
@dataclass
class Foo:
foo: Annotated[str, Constraint]
@post()
async def post_handler(data: Foo) -> None:
...
app = Litestar([post_handler], debug=True) And via DTOs the constraint would also be enforced. So does this: from dataclasses import dataclass
from typing_extensions import Annotated
from litestar import Litestar, get
class Constraint:
max_length = 3
@get()
async def get_handler(arg: Annotated[str, Constraint]) -> None:
...
app = Litestar([get_handler], debug=True) For parameters, we should only parse constraints from Given how permissive we've been parsing metadata so far, I think any work to tighten this up needs to be against the 3.0 branch. |
This PR aims to change our approach of parsing schema metadata from an indiscriminate ducktype approach to a deliberate approach with situational awareness. For parameter declarations, we should only parse schema metadata from instances of `KwargDefinition` and sub-types. For model parsing, including dataclasses, typeddict and 3rd party modelling libraries, we should only parse metadata according to that modelling libraries supported methods of constraint delivery. For #3202
That's a really great investigation @peterschutt In short, what I've seen from the codebase, crafting the OpenAPI spec via The parsing should really be much more strict (and typed) as suggested. 💯 for that. |
Summary
https://docs.litestar.dev/2/usage/requests.html#request-body states that:
More on 1:
By "support for dataclasses/TypedDicts" you'd expect to be able to declare OpenAPI constraints and extra validations somehow, e.g.:
The generated OpenAPI schema output is missing
default
in many cases (but that's a separate issue, see #3201).The thing is, how can you:
Pydantic models have
Field
, Msgspec hasMeta
. Dataclasses havedataclass.field
but you cannot specify OpenAPI constraints through that. TypedDicts don't have that, and cannot even have default values.It would appear that you can just use
Parameter
orKwargDefinition
in TypedDicts and dataclasses as shown in the above snippet, and maybe it (mostly) works? But this should be instructed in the docs (and decision made if that is intended as supported or not). Also, that would allow to define a default also for TypedDict (viaKwargDefinition(default=...)
but then that actually doesn't work. Misleading APIs like that should be blocked (although maybe that's a quite special case).Note
While we are open for sponsoring on GitHub Sponsors and
OpenCollective, we also utilize Polar.sh to engage in pledge-based sponsorship.
Check out all issues funded or available for funding on our Polar.sh dashboard
The text was updated successfully, but these errors were encountered: