strawberry-graphql / strawberry

A GraphQL library for Python that leverages type annotations 🍓
https://strawberry.rocks
MIT License
3.85k stars 511 forks source link

Input type is not JSON serializable #780

Closed mecampbellsoup closed 1 year ago

mecampbellsoup commented 3 years ago

Hi @patrick91 -

I'm at a loss for what is going on here.

I've defined an object input type SubscriptionForm which I am using in a mutation resolver as follows:

@strawberry.input
class PaymentProfileAttributes:
    first_name: str
    last_name: str
    full_number: str
    cvv: str
    expiration_month: str
    expiration_year: str
    billing_zip: str

@strawberry.input
class CustomerAttributes:
    first_name: str
    last_name: str
    email: str
    phone: Optional[str]
    organization: str
    reference: str

@strawberry.input
class SubscriptionForm:
    agree_to_terms: bool
    payment_profile_attributes: PaymentProfileAttributes
    customer_attributes: CustomerAttributes
    product_handle: str
    reference: str

@strawberry.type
class Mutation:
    @strawberry.mutation
    async def subscribe(self, info: Info, form: SubscriptionForm) -> APIResponse:
        ...

However the graphql server is erroring with:

{"data":null,"errors":[{"message":"Object of type SubscriptionForm is not JSON serializable","locations":[{"line":2,"column":3}],"path":["subscribe"]}]}

The object being sent to the server looks as follows:

{
    "subscription": {
        "agreeToTerms": false,
        "paymentProfileAttributes": {
            "firstName": "",
            "lastName": "",
            "fullNumber": "",
            "cvv": "",
            "expirationMonth": "",
            "expirationYear": "",
            "billingZip": ""
        },
        "customerAttributes": {
            "firstName": "",
            "lastName": "",
            "email": "mecampbell25@gmail.com",
            "phone": "",
            "organization": "44a18ce7b0",
            "reference": "mecampbell25@gmail.com"
        },
        "productHandle": "coreweave-cloud",
        "reference": "tenant-ef1b7290f0"
    }
}

Am I missing something obvious here?

mecampbellsoup commented 3 years ago

@BryceBeagle @jkimbo maybe you guys can grok what I'm doing wrong?

patrick91 commented 3 years ago

@mecampbellsoup I've tried this with both starlette and django, it seems to be working fine.

maybe there's something in your resolver that's causing the issue? or do you have a custom django view?

mecampbellsoup commented 3 years ago

@patrick91 here is my resolver:

@strawberry.type
class APIResponse:
    ok: bool
    errors: List[Optional[Error]]

@strawberry.type
class Mutation:
    @strawberry.mutation
    async def subscribe(self, info: Info, form: SubscriptionForm) -> APIResponse:
        form_data = form
        base_url = os.getenv('CHARGIFY_API_URL')
        async with httpx.AsyncClient(base_url=base_url) as client:
            api_key = os.getenv('CHARGIFY_API_KEY')
            auth_str = base64.b64encode(f'{api_key}:x'.encode('utf-8')).decode('utf-8')
            headers = {
                'Authorization': f'Basic {auth_str}',
                'Content-Type': 'application/json'
            }
            resp: HttpxResponse = await client.post('/subscriptions.json', headers=headers, json=form_data)
            status: int = resp.status_code
            print(status)
            body: dict = resp.json()
            print(body)
patrick91 commented 3 years ago

oh yes, this is the line that's broken:

resp: HttpxResponse = await client.post('/subscriptions.json', headers=headers, json=form_data)

you can do instead (after importing dataclasses:

resp: HttpxResponse = await client.post('/subscriptions.json', headers=headers, json=dataclasses.asdict(form_data))
mecampbellsoup commented 3 years ago

OMG, thank you so much Patrick!!

How did you know that was the issue? Is this something I could add to the docs?

patrick91 commented 3 years ago

OMG, thank you so much Patrick!!

How did you know that was the issue? Is this something I could add to the docs?

I've seen that error a few times :) but I think also we haven't really documented well the fact that we can convert strawberry types to dicts with dataclasses.asdict (it's implicit by the fact we use dataclasses). I wonder if we can do something better here (we could have our own asdict function or maybe a method on the types as well) 🤔

BryceBeagle commented 3 years ago

We might just want to implement our own JSONEncoder that knows how to deal with Strawberry types

patrick91 commented 3 years ago

@BryceBeagle do you prefer that to converting types to dicts?

BryceBeagle commented 3 years ago

Probably, but only marginally

patrick91 commented 3 years ago

Converting things to dicts might retain types like dates and so on, which might be useful in somecases (also it should be faster than converting things to json all the time).

BryceBeagle commented 3 years ago

But here, the use case is explicitly for json, so I think it would be best to do the recursive conversion all the way down the object tree

patrick91 commented 3 years ago

But here, the use case is explicitly for json, so I think it would be best to do the recursive conversion all the way down the object tree

httpx will do the actual json conversion here: https://github.com/encode/httpx/blob/a3eb0f99dc112fb5b09c7d6fdb9d211f96a6e3c1/httpx/_content.py#L175

the json parameter expects a dict to be converted to json 😊

nrbnlulu commented 1 year ago

2170