cercide / fastapi-xml

adds xml support to fastapi
MIT License
12 stars 2 forks source link

FastAPI with xsData generated classes raises FastAPIError #2

Closed skinkie closed 1 year ago

skinkie commented 1 year ago

I just by accident found your project, and I would consider the approach to be both interesting and useful. As a xsData user I am very interested in this bridge. You write that FastAPI-XML supports xsData classes "Together, fastapi handles xml data structures using dataclasses generated by xsdata."

I have created ``xsdata generate -p ojp -ss clusters --compound-fields /home/skinkie/Sources/OJP/OJP.xsd``` I have modified the "HelloWorld" example with my root element "Ojp". Still I end up with the error below. What would create the full end to end solution to support below?

@[app.post](http://app.post/)("/ojp2023", response_model=Ojp, tags=["Open Journey Planner"])
def echo(x: Ojp = XmlBody()) -> Ojp:
    return x
fastapi.exceptions.FastAPIError: Invalid args for response field! Hint: check that <class 'ojp.ojp.Ojp'> is a valid Pydantic field type. If you are using a return type annotation that is not a valid Pydantic field (e.g. Union[Response, dict, None]) you can disable generating the response model from the type annotation with the path operation decorator parameter response_model=None. Read more: https://fastapi.tiangolo.com/tutorial/response-model/

Your HelloWorld looks like:

@dataclass
class HelloWorld:
    message: str = field(metadata={"example": "Foo","name": "Message", "type": "Element"})

While the generated Ojp interface looks like below.

@dataclass
class Ojp:
    """
    Root element for OJP messages based on SIRI message exchange protocol.
    """
    class Meta:
        name = "OJP"
        namespace = "http://www.siri.org.uk/siri"

    ojprequest: Optional[Ojprequest] = field(
        default=None,
        metadata={
            "name": "OJPRequest",
            "type": "Element",
        }
    )
    ojpresponse: Optional[Ojpresponse] = field(
        default=None,
        metadata={
            "name": "OJPResponse",
            "type": "Element",
        }
    )
    extensions: Optional[Extensions2] = field(
        default=None,
        metadata={
            "name": "Extensions",
            "type": "Element",
        }
    )
    version: str = field(
        init=False,
        default="1.1-dev",
        metadata={
            "type": "Attribute",
            "required": True,
        }
    )
cercide commented 1 year ago

Thanks for your interest in this project and your detailed error description. It helped a lot to figure out what's going on. Based on the code provided there is a problem with the medadata of Ojp.version. Fastapi converts any dataclass into a pydantic model. Thereby, any metadata field gets passed to the pydantic field. However, the keyword required is reserved for these fields and causes an error if available in a dataclass. Remove the keyword required and it should work.

Nevertheless, the classes Ojprequest, Ojpresponse, and Extensions2 might cause errors too. Since I do not have any information about these classes I can only tell that the following code without the required keyword works a least. Please let me know if this helps you out.

from dataclasses import dataclass, field
from fastapi import FastAPI
from fastapi_xml import add_openapi_extension
from fastapi_xml import NonJsonRoute
from fastapi_xml import XmlAppResponse
from fastapi_xml import XmlBody
from typing import Optional

@dataclass
class Ojprequest:
    message: str = field(metadata={"example": "Foo","name": "Message", "type": "Element"})

@dataclass
class Ojpresponse:
    message: str = field(metadata={"example": "Foo","name": "Message", "type": "Element"})

@dataclass
class Extensions2:
    message: str = field(metadata={"example": "Foo","name": "Message", "type": "Element"})

@dataclass
class Ojp:
    """
    Root element for OJP messages based on SIRI message exchange protocol.
    """
    class Meta:
        name = "OJP"
        namespace = "http://www.siri.org.uk/siri"
    ojprequest: Optional[Ojprequest] = field(
        default=None,
        metadata={
            "name": "OJPRequest",
            "type": "Element",
        }
    )
    ojpresponse: Optional[Ojpresponse] = field(
        default=None,
        metadata={
            "name": "OJPResponse",
            "type": "Element",
        }
    )
    extensions: Optional[Extensions2] = field(
        default=None,
        metadata={
            "name": "Extensions",
            "type": "Element",
        }
    )
    version: str = field(
        init=False,
        default="1.1-dev",
        metadata={
            "type": "Attribute"
        }
    )

@dataclass
class HelloWorld:
    message: str = field(metadata={"example": "Foo","name": "Message", "type": "Element"})

app = FastAPI(title="FastAPI::XML", default_response_class=XmlAppResponse)
app.router.route_class = NonJsonRoute
add_openapi_extension(app)

@app.post("/echo", response_model=Ojp, tags=["Example"])
def echo(x: HelloWorld = XmlBody()) -> HelloWorld:
    x.message += " For ever!"
    return x

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000)
skinkie commented 1 year ago

I rather not change the output of the generated code. That would circumvent the idea of full integration. The full schema can be found below.

https://github.com/VDVde/OJP/releases/tag/v1.0.1

cercide commented 1 year ago

Understandable. Nevertheless, the given exception cannot be solved within the scope of this project without modifying the generated code. This is an error caused outside of fastapi-xml; Either by pyndatic while converting the model, or by xsdata while generating it. Consequently, I consider this issue as closed.

skinkie commented 1 year ago

The point is that an xsd with a minOccurs will fail. I consider that not a general solution, now I have 'resolved' it unelegantly with the Response and Request, but there must be a way to make this more generic.

https://github.com/openTdataCH/ojp-nova/blob/master/server.py#L21