Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

Interface resolver issues with Pydantic schemas #1179

Closed
Verhaeg opened this issue May 9, 2024 · 2 comments
Closed

Interface resolver issues with Pydantic schemas #1179

Verhaeg opened this issue May 9, 2024 · 2 comments

Comments

@Verhaeg
Copy link

Verhaeg commented May 9, 2024

I was facing an issue using and defining an interface in GraphQL when used together with Pydantic. The solution I finally managed to wotk with will not scale very well (not elegantly nor pythonic IMO).

PS: using latest versions of Flask (3.0.3), Pydantic (2.7.1) and Ariadne (0.23.0).

The basic GraphQL:

interface Metadata {
  subject: String
  date: String
}

type MetadataCommon implements Metadata {
  subject: String
  date: String
}

type MetadataBR implements Metadata {
  subject: String
  date: String

  type: String
}

type Information {
  id: String
  metadata: Metadata!
}

type Response {
  informations: [Information!]
}

type Query {
  search (data: SearchInput) Response!
}

Basic Pydantic schemas:

class Metadata(BaseModel):
    date: Optional[date] = None
    subject: Optional[str] = None

class MetadataBR(Metadata):
    type: Optional[str] = None

class Information(BaseModel):
    id: str
    metadata: Metadata

class Response(BaseModel):
    informations: List[Information]

The interface resolver:

interface_metadata = InterfaceType("Metadata")

@interface_metadata.type_resolver
def resolve_metadata_type(obj, *_):
    if isinstance(obj, schemas.MetadataBR):
        return 'MetadataBR'
    return 'MetadataCommon' # Returning the base value just for testing, could be None

In my code, currently I generate only MetadataBR. But with the current definition of the Information model for Pydantic (where I define metadata being any class that can be evaluated to a base class and I can increase the number of possible classes in the future without adding new possible combinations to this field using Union). When the code reaches the resolver function, obj for some reason comes with type schemas.Metadata and without the type field that is setted in the code.

Somewhere inside Ariadne code, it is converting my code to another type because if in my resolver, after I fetch the information and before returning the values, I try to dump the types and values, they all come correctly (MetadataBR and with type field).

What I had to do to make it work was change my python model to:

class Information(BaseModel):
    id: str
    metadata: MetadataBR

If I increase the number of metadata sub models, I would need to keep adding to several Unions.. making it not so readable.

Not sure if I missed something, but would be nice to have that working.

Thanks

@DamianCzajkowski
Copy link
Contributor

Thanks for pointing this out. I'll take a look at it soon and let you know if it's a bug or if there's something that can be adjusted in the code. I'll get back to you as soon as I can.

@DamianCzajkowski DamianCzajkowski added the bug Something isn't working label May 14, 2024
@DamianCzajkowski
Copy link
Contributor

@Verhaeg I suggest trying this approach, which omits resolvers. This code allows Pydantic to determine which metadata is returned. If you'd like, you can also try using ariadne-codegen.

from typing import List, Literal, Optional, Union

from pydantic import Field, BaseModel


class Search(BaseModel):
    search: "Response"


class Response(BaseModel):
    informations: Optional[List["Information"]]


class Information(BaseModel):
    metadata: Union[
        "Metadata",
        "MetadataBR",
    ] = Field(discriminator="typename__")


class Metadata(BaseModel):
    typename__: Literal["Metadata", "MetadataCommon"] = Field(alias="__typename")
    subject: Optional[str]
    date: Optional[str]


class MetadataBR(BaseModel):
    typename__: Literal["MetadataBR"] = Field(alias="__typename")
    subject: Optional[str]
    date: Optional[str]
    type: Optional[str]


Search.model_rebuild()
Response.model_rebuild()
Information.model_rebuild()

Example query:

query search {
  search {
    informations {
      metadata {
        subject
        date
        ... on MetadataBR {
          type
        }
      }
    }
  }
}

@DamianCzajkowski DamianCzajkowski added discussion and removed bug Something isn't working labels Jun 13, 2024
@mirumee mirumee locked and limited conversation to collaborators Jul 4, 2024
@DamianCzajkowski DamianCzajkowski converted this issue into discussion #1187 Jul 4, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Projects
None yet
Development

No branches or pull requests

2 participants