You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I feel obligated to start by saying that this isn't production friendly code, it's an itch that needed scratching, and that there are other ways to solve this problem such as writing a Depends just as one of many examples. I'm strictly exploring the option of directly supporting complex objects in path elements of FastAPI as shown here:
In order to explore this, the only way I found possible for FastAPI to support direct type annotations like this, was to perform some sacrilegious interventions of FastAPI definitions (and this is where it will briefly go outside of what anyone should do, but bear with me):
importfastapiimportpydanticfromfastapi.exceptionsimportRequestValidationErrorfromdataclassesimportdataclassfromtyping_extensionsimportLiteralapp=fastapi.FastAPI()
classExampleCustomType(pydantic.BaseModel):
""" Represents a "complex object" that can convert a string to a BaseModel. Doesn't have to be a BaseModel, but for demonstration purpose. """type :strid :strdef__repr__(self):
returnf"{self.type}:{self.id}"def__str__(self):
returnself.__repr__()
def__init__(self, *args, **kwargs):
""" This is a helper __init__ to convert * ExampleCustomType("sometype:some-identifier") * ExampleCustomType(type="sometype", id="some-identifier") Into a normalized object initation of ExampleCustomType(type="sometype", id="some-identifier") """ifnotlen(args_value:= (next(iter(list(args)+[''])))) andnot (kwargs_value:= (':'.join(kwargs.values()) ifkwargselse [])):
raiseValueError(f"{args_valueorkwargs_value} is not a ExampleCustomType.")
super().__init__(**next(({'type': split[0], 'id': split[1]} forsplitin [args_value.split(':', 1) ifargs_value!=''elsekwargs_value.split(':', 1)])))
# This is our "complex object" which can handle:assertExampleCustomType("sometype:some-identifier").__class__==ExampleCustomTypeassertExampleCustomType(type="sometype", id="some-identifier").__class__==ExampleCustomTypeclassFastAPIInterceptor:
""" FastAPI refuses "complex objects" in paths such as: @app.get("/shares/{obj}/objects") def route(obj :ExampleCustomType): Where we typically would expect `obj` to become a `ExampleCustomType()` instance automatically (if it was supported). This is how it works if `obj` was for instance a `pathlib.Path()` type. However, FastAPI only works with known types/classes for parameters and to my knowledge there's no implemented way or a generic override for typechecking or a "i assume all responsabilities" flag. We can however introduce this ourselves by: 1. Via intercept_field_annotation_is_scalar() replace typechecking on route definitions meaning @app.get("/objs/{obj}/objects") 2. Via intercept_create_response_field() momentary start transaction from step 3 before we let FastAPI call FastAPI's normal create_response_field() 3. intercept_ModelField() provides a transactional replacement of ModelField() used by FastAPI.create_response_field() which is what validates the actual parameter. Once the fake_ModelField() has been created and sent off to __original_create_response_field(), ModelField() gets reverted to it's original state .. note:: This makes FastAPI behave as normal in general, and only gets this patched behavior if the <annotated type>.__file__ == __file__ - meaning it's created here, in this, file by us! """# Store original definitions to be able to revert__original_create_response_field=fastapi.dependencies.utils.create_response_field__original_ModelField=fastapi.utils.ModelField@staticmethoddefintercept_field_annotation_is_scalar():
_original=fastapi._compat.field_annotation_is_scalarfastapi._compat.field_annotation_is_scalar=lambdaannotation: annotation==ExampleCustomTypeor_original(annotation)
@staticmethoddefintercept_ModelField():
classtransaction:
def__enter__(self):
fastapi.utils.ModelField=self.fake_ModelFieldreturnselfdef__exit__(self, *args, **kwargs):
fastapi.utils.ModelField=FastAPIInterceptor._FastAPIInterceptor__original_ModelFielddeffake_ModelField(self, *args, **kwargs):
return_ModelField_Hook(*args, **kwargs)
returntransaction()
@staticmethoddefintercept_create_response_field():
defstubidub(*args, **kwargs):
importinspecttry:
# By checking if the annotated type is from this __file__,# we should be some what safe that it's our custom example type and thus process it with# a patch transaction of ModelField()ifinspect.getfile(kwargs.get('type_')) ==__file__:
withFastAPIInterceptor.intercept_ModelField() astransaction:
returnFastAPIInterceptor._FastAPIInterceptor__original_create_response_field(*args, **kwargs)
exceptTypeError:
pass# Builtins causes this: https://docs.python.org/3/library/inspect.html#inspect.getfilereturnFastAPIInterceptor._FastAPIInterceptor__original_create_response_field(*args, **kwargs)
fastapi.dependencies.utils.create_response_field=lambda*args, **kwargs: stubidub(*args, **kwargs)
@dataclassclass_ModelField_Hook(FastAPIInterceptor._FastAPIInterceptor__original_ModelField):
""" ModelField() would normally setup av TypeAdapter() and call TypeAdapter().validate_python(value, from_attributes=True) which in turn would initiate Pydantic validation through pydantic-core. And this would fail, since ExampleCustomType()'s type annotation won't be correct. However, because we're aware of this in our setup we can process validation manually for this object. Thus, we simply need to intercept the .validate() call to ensure creation is valid. """defvalidate(self, value, values, *, loc):
""" This is where we convert the path object from `str` to `complexObj()` and also perform validation internally within the ExampleCustomType(). """try:
return (self.field_info.annotation(**value), None) ifisinstance(value, dict) else (self.field_info.annotation(value), None)
exceptExceptionaserror:
validation_error=RequestValidationError([{
"type": "complex_obj_error",
"loc": ("path_parameter", self.field_info.alias),
"msg": str(error),
"input": {},
"ctx": {"error": error.__class__.__name__},
}],
body=None
)
raisevalidation_errorfromerror# Patch FastAPI to allow for our behaviorFastAPIInterceptor.intercept_field_annotation_is_scalar()
FastAPIInterceptor.intercept_create_response_field()
@app.get("/test/{obj}/path")asyncdefget_test_path(obj :ExampleCustomType):
print(f"Processing {type(obj)}: {obj}")
return {}
This allows for direct type annotation of path elements with possibility of internally handling validation within the ExampleCustomType type.
And what I wanted to do here was to discuss if this type of direct annotation would be desirable? What kind of issues could come from allowing these complex types here?
I'm assuming there's a reason for why complex objects are not supported. Or is the "only reason" the fact that pydantic-core can't perform validation against the internal type annotation of type and id because the input is a single str and that's it?
And if so, would a app = fastapi.FastAPI(allow_complex_types=True) be a feasible "advanced" feature that developers could leverage to assume responsibility of performing type conforming on the input data during initialization of the obj?
Again, this was a intrusive thought that I wanted to explore, and perhaps there is an actual intended and supported useage for this already and I missed it?
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
-
I feel obligated to start by saying that this isn't production friendly code, it's an itch that needed scratching, and that there are other ways to solve this problem such as writing a Depends just as one of many examples. I'm strictly exploring the option of directly supporting complex objects in path elements of FastAPI as shown here:
But to avoid the following (expected) error:
In order to explore this, the only way I found possible for FastAPI to support direct type annotations like this, was to perform some sacrilegious interventions of FastAPI definitions (and this is where it will briefly go outside of what anyone should do, but bear with me):
This allows for direct type annotation of path elements with possibility of internally handling validation within the ExampleCustomType type.
And what I wanted to do here was to discuss if this type of direct annotation would be desirable? What kind of issues could come from allowing these complex types here?
I'm assuming there's a reason for why complex objects are not supported. Or is the "only reason" the fact that pydantic-core can't perform validation against the internal type annotation of
type
andid
because the input is a singlestr
and that's it?And if so, would a
app = fastapi.FastAPI(allow_complex_types=True)
be a feasible "advanced" feature that developers could leverage to assume responsibility of performing type conforming on the input data during initialization of the obj?Again, this was a intrusive thought that I wanted to explore, and perhaps there is an actual intended and supported useage for this already and I missed it?
Beta Was this translation helpful? Give feedback.
All reactions