DeanWay / pydantic-jsonapi

an implementation of JSON:api using pydantic for validation
MIT License
70 stars 10 forks source link

RFE: constructor from primitive model #10

Closed jonathanunderwood closed 5 years ago

jonathanunderwood commented 5 years ago

Is your feature request related to a problem? Please describe. Constructing a request/response object is more arduous than it needs to be, and can't be done from ORM models.

Describe the solution you'd like

class Item(BaseModel):
    name: str
    quantity: int
    price: float

ItemRequest, ItemResponse = JsonApiModel('item', Item)

item = Item(name="my item", quantity=4, price=1.2)
item_req = ItemRequest(item)

This would also make it super easy to construct request/response objects from ORM models.

It wouldn't necessarily have to be the constructor, it could be a different class method e.g. item_req = ItemRequest.from_primitive(item)

Describe alternatives you've considered Right now, I mangle by hand.

Additional context Add any other context or screenshots about the feature request here.

DeanWay commented 5 years ago

Hi @jonathanunderwood thanks for the submission. I can certainly attempt to implement something like what you suggested, and have been thinking about it as well. Just not sure yet how much magic makes sense to add here. For example, a somewhat logical next step for a feature like this is to add some handling or discovery of relationships, includes, and links. Most JSON api implementations try to do this in some way or another. For now, this library is focused mainly on structural typing and validation of JSON api documents, but I could see it providing more data transformation functionality.

If you're interested, this is the sort of approach I've been taking on this:

from pydantic import BaseModel
from pydantic_jsonapi import JsonApiModel
from my_data_model import Item

class ItemAttributes(BaseModel):
    name: str
    quantity: int
    price: float

    class Config:
        orm_mode = True

ItemRequest, ItemResponse = JsonApiModel('item', ItemAttributes)

def marshal_item(item: Item) -> dict:
    return {
        'id': item.id,
        'type': 'item',
        'attributes': ItemAttributes.from_orm(item).dict()
        'relationships': {
            'sold_at': {
                'data': {'type': 'store', 'id': item.store.id}
            }
        }
    }

# some http endpoint
@app.get('/item')
def get_item():
    item = db.query(Item).one()
    return ItemResponse(data=marshal_item(item)).json()

There's some amount of duplication with this approach, but it's very flexible in how you want to use json api and there's no magic. Any structure you produce with a function like marshal_item here ends up getting validated against the JSON api spec

jonathanunderwood commented 5 years ago

Yes, that's essenetially the approach I have been taking. In your example, presumably your ItemAttributes would also have an id field, which is Optional ?

ngseer commented 5 years ago

@jonathanunderwood id field is a bit tricky since it's not considered an attribute in JSONAPI.

jonathanunderwood commented 5 years ago

Incidentally, the approach you outline is for marshalling from an ORM model directly. Which is a partial solution. But I find myself often wanting to create a JSONAPI response/request from the corresponding attributes pydantic object.

DeanWay commented 5 years ago

@jonathanunderwood please take a look at #11 I've added a new method resource_object, which comes pretty close to your request here.

With that change, my above example could be changed to look something like this:

ItemRequest, ItemResponse = JsonApiModel('item', ItemAttributes)

# some http endpoint
@app.get('/item')
def get_item():
    item = db.query(Item).one()
    return ItemResponse(
        data=ItemResponse.resource_object(
            id=item.id, 
            attributes=item
        )
    ).json()

or for lists:


@app.get('/items')
def get_items():
    items = db.query(Item).all()
    return ItemResponse(
        data=[
            ItemResponse.resource_object(
                id=item.id, 
                attributes=item
            ) for item in items
        ]
    ).json()
DeanWay commented 5 years ago

released #11 in 0.9.0

jonathanunderwood commented 5 years ago

This looks very handy, thanks for adding it. I'll give it a try next week. Thanks again.

jonathanunderwood commented 4 years ago

We've been using this locally for a few days, and it works very well! Many thanks for adding it.