strawberry-graphql / strawberry

A GraphQL library for Python that leverages type annotations šŸ“
https://strawberry.rocks
MIT License
4.01k stars 531 forks source link

Support Dict type field #754

Closed Dotrox closed 3 years ago

Dotrox commented 3 years ago

I have the following pydantic model:

class SourceModel(BaseModel):
    id: str
    data: Dict[str, Any]

And use it to define Strawberry type:

@strawberry.experimental.pydantic.type(model=SourceModel, fields=['id', 'data'])
class Source:
    pass

But I get the following error:

Traceback (most recent call last):
  File "schema.py", line 87, in <module>
    class Source:
  File "/lib/python3.8/site-packages/strawberry/experimental/pydantic/type.py", line 61, in wrap
    all_fields = [
  File "/lib/python3.8/site-packages/strawberry/experimental/pydantic/type.py", line 64, in <listcomp>
    get_type_for_field(field),
  File "/lib/python3.8/site-packages/strawberry/experimental/pydantic/type.py", line 36, in get_type_for_field
    type_ = replace_pydantic_types(type_)
  File "/lib/python3.8/site-packages/strawberry/experimental/pydantic/type.py", line 22, in replace_pydantic_types
    return type_.copy_with(tuple(replace_pydantic_types(t) for t in type_.__args__))
  File "/lib/python3.8/site-packages/strawberry/experimental/pydantic/type.py", line 22, in <genexpr>
    return type_.copy_with(tuple(replace_pydantic_types(t) for t in type_.__args__))
  File "/lib/python3.8/site-packages/strawberry/experimental/pydantic/type.py", line 24, in replace_pydantic_types
    if issubclass(type_, BaseModel):
  File "/usr/local/lib/python3.8/abc.py", line 102, in __subclasscheck__
    return _abc_subclasscheck(cls, subclass)
TypeError: issubclass() arg 1 must be a class

Field data in my model is embedded document from MongoDB and is structure may varies (I get it from external source), so I can use only Dict for it.

In Graphene I use GenericScalar type for it and it perfect serve my needs. But I want switch to Strawberry and can't without this.

patrick91 commented 3 years ago

@Dotrox I think we can implement that šŸ˜Š I might have some time over the weekend to do so.

I need to think a bit about the implication of doing this for the Dict type, as it might be confusing to some people.

Does Graphene convert to GenericScalar directly?

Dotrox commented 3 years ago

Does Graphene convert to GenericScalar directly?

I can`t use pydantic models with Graphene, so define all fields directly by myself:

class Source(graphene.ObjectType):
    id = graphene.String()
    data = GenericScalar()

With Strawberry I already use additional decorator for types because I use not pure pydantic BaseModel, but wrapped model class from ODMantic ODM. This how it really looks now:

def init_model(model):
    def wrap(cls):
        cls_annotations = model.__pydantic_model__.__dict__.get(
            "__annotations__", {})

        for field, field_type in cls_annotations.items():
            if field_type == odmantic.bson._datetime:
                cls_annotations[field] = datetime
            elif field_type == odmantic.bson.ObjectId:
                cls_annotations[field] = str

        cls.__annotations__ = cls_annotations

        return cls

    return wrap

@strawberry.type
@init_model(SourceModel)
class Source:
    pass

Only this way Strawberry can perfectly works with ODMantic models: I need support for MongoDB ObjectId and also ODMantic implicitly change datetime with own version. And as sugar my decorator load all fields from my model, but main reason for it is type conversion.

So Strawberry may support Dict type in similar way (I tried implement it by myself, but my knowledges not enough).

patrick91 commented 3 years ago

ok! thanks for the additional information, will take a look at it soon :)

Dotrox commented 3 years ago

Any news?

patrick91 commented 3 years ago

@Dotrox haven't had time to look into this yet, is it blocking you?

Dotrox commented 3 years ago

I can't use Strawberry without this. Maybe you can explain how I can implement this by myself (how add custom type to Strawberry)?

marcoacierno commented 3 years ago

What should be the result of data? A JSON string?

Dotrox commented 3 years ago

What should be the result of data? A JSON string?

No, it must be an object. As Strawberry Object type, but without predefined fields.

Speedy1991 commented 3 years ago

Just added some documentations about an example JSONScalar here

patrick91 commented 3 years ago

@Dotrox let me know if the docs written by @Speedy1991 are helpful; so we can close this issue šŸ˜Š

Dotrox commented 3 years ago

@Dotrox let me know if the docs written by @Speedy1991 are helpful; so we can close this issue blush

Json doesn't fit my needs, but this example show me how create a custom scalar. So I slightly modified it and get what I need:

GenericScalar = scalar(
    typing.NewType("GenericScalar", typing.Any),
    description="The GenericScalar scalar type represents a generic GraphQL scalar value that could be: List or Object."
)

Also I made a custom scalar for MongoDB ObjectId:

ObjectIdScalar = scalar(
    bson.ObjectId,
    serialize=lambda v: str(v),
    parse_value=lambda v: bson.ObjectId(v),
    description="MongoDB ObjectId"
)

It may be helpful for somebody if it will be in docs.

lovetoburnswhen commented 2 years ago

Here's what I needed to do to get JSONScalar working with an inline input value

JSONScalar = strawberry.scalar(
    NewType("JSONScalar", Any),
    serialize=lambda v: v,
    parse_value=lambda v: json.loads(v),
    parse_literal=graphql.utilities.value_from_ast_untyped,
)