Upgrading to Flask-restless v1.0.0


Flask-restless is a Flask extension providing generation of REST APIs from SQLAlchemy-defined database models. Flask-restless v1.0.0 introduces adoption of the jsonapi.org spec, providing additional features, consistent and strict HTTP Method contracts, at the expense of inherent breaking changes.

This article walks through the most basic of breaking changes found while upgrading from Flask-restless v0.17 to Flask-restless v1.0.0.

Flask App and Models

For purposes of this walkthrough, we assume a basic understanding of SQLAlchemy, and Flask. We define our Flask app in the following file:


from flask import Flask
import flask_restless
import flask_sqlalchemy
import simplejson as json

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/flask_restless.db'
db = flask_sqlalchemy.SQLAlchemy(app)


class ProgrammingLanguage(db.Model):
    __tablename__ = "programming_languages"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String, unique=True)
    latest_release = db.Column(db.String)

db.create_all()
# Create the Flask-Restless API manager.
manager = flask_restless.APIManager(app, flask_sqlalchemy_db=db)
manager.create_api(ProgrammingLanguage, methods=['GET', 'POST', 'PATCH', 'DELETE'])

if __name__ == "__main__":
    # Start the server with 'python \<name_of_this_file.py\>'
    app.run()


POSTing a row

Throughout the walkthrough, we will compare code samples of requests and responses between Flask-restless v0.17 (left), and v1.0.0 (right).

More specifically, we highlight the updates required to get your code working with v1.0.0. We start with some basic legacy v0.17 code to POST a programming_language Resource Object to our database.

data = {
    "name": "Python",
    "latest_release": "3.6.0"
}
url = "http://localhost:5000/api/programming_languages"
headers = {"Content-Type": "application/json"}
resp = requests.post(url, simplejson.dumps(data), headers=headers)

Update #1: Content-Type Header

The response content that we receive varies dramatically between Flask-restless v0.17 (left) and v1.0.0 (right).


# Flask v0.17 Response
>>> resp.status_code
201
>>> resp.content
{
    "id": 1,
    "name": "Python",
    "latest_release": "3.6.0" 
}

# Flask v1.0.0 Response
>>> resp.status_code
415
>>> resp.content
{
    "errors": [
        {
            "code": null,
            "detail": "Request must have \"Content-Type: application/vnd.api+json\" header",
            "id": null,
            "links": null,
            "meta": null,
            "source": null,
            "status": 415,
            "title": null
        }
    ],
    "jsonapi": {
        "version": "1.0"
    }
}

Quite clearly, we see that we did not set the Content-Type header according to the jsonapi.org spec, so we get a 415 - Unsupported Media Type status.

Let’s set the header to application/vnd.api+json below and continue…

Update #2: JSON wrapped with top-level “data” key


data = {
    "name": "Python",
    "latest_release": "3.6.0"
}
url = "http://localhost:5000/api/programming_languages"
headers = {"Content-Type": "application/vnd.api+json"}

resp = requests.post(url, simplejson.dumps(data), headers=headers)

>>> resp.status_code
400
>>> resp.content
{
    "errors": [
        {
            "code": null,
            "detail": "Failed to deserialize object: missing \"data\" element",
            "id": null,
            "links": null,
            "meta": null,
            "source": null,
            "status": 400,
            "title": null
        }
    ],
    "jsonapi": {
        "version": "1.0"
    }
}

This time we see a 400 - Bad Request status, which generally means that our JSON payload could not be understood by the server. Specifically, the error states:

"Failed to deserialize object: missing \"data\" element"

The JSON API spec expects any POST payload to be delivered with its top-level schema of the form {"data": {....}}, so let's update our payload as follows and try again.

Update #3: Provide the “type” of our POSTed object


data = {
    "name": "Python",
    "latest_release": "3.6.0"
}
url = "http://localhost:5000/api/programming_languages"
headers = {"Content-Type": "application/vnd.api+json")

resp = requests.post(url, simplejson.dumps({"data": programming_language_obj}), headers=headers)

>>> resp.status_code
400
>>> resp.content
{
    "errors": [
        {
            "code": null,
            "detail": "Failed to deserialize object: missing \"type\" element",
            "id": null,
            "links": null,
            "meta": null,
            "source": null,
            "status": 400,
            "title": null
        }
    ],
    "jsonapi": {
        "version": "1.0"
    }
}

Again, we get a 400 status, with the server telling us:

"Failed to deserialize object: missing \"type\" element"

The JSON API spec requires us to provide the type of resource that we are creating with our POST request.

In this case, we are posting to the programming_languages table, so let's update our query accordingly...

Update #4: Provide “attributes” of our POSTed object


data = {
    "name": "Python", 
    "latest_release": "3.6.0"
    "type": "programming_languages"
}
url = "http://localhost:5000/api/programming_languages"
headers = {"Content-Type": "application/vnd.api+json"}

resp = requests.post(url, simplejson.dumps({"data": data}), headers=headers)

>>> resp.status_code
201
>>> resp.content
{
    "data": {
        "attributes": {
            "name": null,
            "latest_release" null
        },
        "id": "1",
        "links": {
            "self": "http://localhost:5001/api/programming_languages/1"
        },
        "relationships": {},
        "type": "programming_languages"
    },
    "included": [],
    "jsonapi": {
        "version": "1.0"
    },
    "links": {},
    "meta": {}
}

Ahhh, finally a 201 - Created status code...wait a minute...if we access resp.content['attributes']['name'] we see that it has a value of null.

But we created our object with {"name": "Python"}, what happened?

The JSON API spec ignores our top-level name parameter - it is looking for the attributes top-level key to determine the attribute values (column values) of our new programming_language resource object.

Let's update our JSON payload one more time, and take a look at the full diff between v0.17 and v1.0.0

POST Request - Full Comparison


# Flask-restless v0.17 POST request
data = {
    "name": "Python",
    "latest_release": "3.6.0"
}
url = "http://localhost:5000/api/programming_languages"
headers = {"Content-Type": "application/json"}

resp = requests.post(url, simplejson.dumps(data), headers=headers)
# Viewing the response of the request
>>> resp.status_code
201
>>> resp.content
{
    "id": 1,
    "name": "Python",
    "latest_release": "3.6.0"
}

There are several fields in our resp.content payload that we have not yet mentioned.

Most notable, is the relationships field nested within the Resource Object (data object). This walkthrough does not cover relationships, but these can be read about here.

You should expect a follow up post covering these more advanced use cases of Flask-restless.


# Flask-restles v1.0.0 POST request
data = {
    "type": "programming_languages",
    "attributes": {
        "name": "Python",
        "latest_release": "3.6.0"
    }
}
url = "http://localhost:5000/api/programming_languages"
headers = {"Content-Type": "application/vnd.api+json"})

resp = requests.patch(url, simplejson.dumps({"data": data}),
                      headers=headers)
# Viewing the response
>>> resp.status_code
201
>>> resp.content
{
    "data": {
        "attributes": {
            "name": "Python",
            "latest_release": "3.6.0"
        },
        "id": "1",
        "links": {
            "self": "http://localhost:5001/api/programming_languages/1"
        },
        "relationships": {}
        "type": "programming_languages"
    },
    "included": [],
    "jsonapi": {
        "version": "1.0"
    },
    "links": {},
    "meta": {}
}


GETting all Rows

Next, we will retrieve the resources that we recently POSTed to the database with a GET request

resp = requests.get("http://localhost:5000/api/programming_languages")

# Flask-restless v0.17
>>> resp.status_code
200
>>> resp.content
{
    "num_results": 1,
    "objects": [
        {
            "id": 1,
            "name": "Python",
            "latest_release": "3.6.0"
        }
    ],
    "page": 1,
    "total_pages": 1
}
           
        

Key differences

  • Size of the resource collection is now givin in resp.content["meta"]["total"]
  • Objects in this collection are nested as a list via resp.content['data']
  • Pagination is handled via resp.content["links"]

Note: getting a specific record is still executed via http://localhost:5000/api/programming_languages/1, where 1 is the id of the record. The return format is equal to that of the POSTed data above

# Flask-restless v1.0.0
>>> resp.status_code
201
>>> resp.content
{
    "data": [
        {
            "attributes": {
                "name": "Python",
                "latest_release": "3.6.0"
            },
            "id": "1",
            "links": {
                "self": "http://localhost:5001/api/programming_languages/1"
            },
            "relationships": {},
            "type": "programming_languages"
        }
    ],
    "included": [],
    "jsonapi": {
        "version": "1.0"
    },
    "links": {
        "first": "http://localhost:5001/api/programming_languages?page%5Bsize%5D=10&page%5Bnumber%5D=1",
        "last": "http://localhost:5001/api/programming_languages?page%5Bsize%5D=10&page%5Bnumber%5D=1",
        "next": null,
        "prev": null,
        "self": "/api/programming_languages"
    },
    "meta": {
        "total": 1
    }
}
           
        

PATCHing a row

We’ve succesfully created and fetched resources from our Flask-restless API - now let’s update our perviously inserted record with a PATCH request. Below is how we would carry this out with Flask-restless v0.17:

# Legacy Flask-restless v0.17 code
data = {
    "latest_release": "3.6.1"
}
url = "http://localhost:5000/api/programming_languages/1"
headers = {"Content-Type": "application/json"}
resp = requests.patch(url, simplejson.dumps(data), headers=headers)

…if you read the POST section above, you know that we’ve got some work to do. Let’s apply Updates #1, #2, #3, and #4 to our PATCH request below to try and get our code working with Flask-restless v1.0.0.

Update #5: Supply Resource ID


data = {
    "type": "programming_languages",
    "attributes": {
        "latest_release": "3.6.1"
    }
}
url = "http://localhost:5000/api/programming_languages/1"
headers = {"Content-Type": "application/vnd.api+json"})

resp = requests.patch(url, simplejson.dumps({"data": data}), headers=headers)
# Print the response
>>> resp.status_code
400
>>> resp.content
{
    "errors": [
        {
            "code": null,
            "detail": "Missing resource ID",
            "id": null,
            "links": null,
            "meta": null,
            "source": null,
            "status": 400,
            "title": null
        }
    ],
    "jsonapi": {
        "version": "1.0"
    }
}
        

Although we've included the id of the resource we'd like to update in the URL, rather redundantly, the JSON API spec for Updates mandates that the "resource object MUST contain id [and type] members".

Let's add the id field to our resource object (the 'data' object in JSON payload), and fire away...

Update #6: Resource ID must be a String


data = {
    "type": "programming_languages",
    "id": 1,
    "attributes": {
        "latest_release": "3.6.1"
    }
}
url = "http://localhost:5000/api/programming_languages/1"
headers = {"Content-Type": "application/vnd.api+json"})

resp = requests.patch(url, simplejson.dumps({"data": data}), headers=headers)
# Print the response
>>> resp.status_code
409
>>> resp.content
{
    "errors": [
        {
            "code": null,
            "detail": "The \"id\" element of the resource object must be a JSON string: 1",
            "id": null,
            "links": null,
            "meta": null,
            "source": null,
            "status": 400,
            "title": null
        }
    ],
    "jsonapi": {
        "version": "1.0"
    }
}
        

Congratulations! You've captured the very elusive 409 - Conflict Status Code!

The spec for this JSON API status code is explained here, but our specific issue is that our ID needs to be a string (so we can support string-based IDs).

Let's change "id": 1 to "id": "1" and carry on...

PATCH Request - Final Comparison


# Flask-restless 0.17
data = {
    "latest_release": "3.6.1"
}
url = "http://localhost:5000/api/programming_languages/1"
headers={"Content-Type": "application/json"}

resp = requests.patch(url, simplejson.dumps(data), headers=headers)

# Print the response
>>> resp.status_code    
200
>>> resp.content
{
    "id": 1,
    "latest_release": "3.6.1",
    "name": "Python"
}

# Flask-restless v1.0.0
data = {
    "type": "programming_languages",
    "id": "1",
    "attributes": {
        "latest_release": "3.6.1"
    }
}
url = "http://localhost:5000/api/programming_languages/1"
headers={"Content-Type": "application/vnd.api+json"}

resp = requests.patch(url, simplejson.dumps({"data": data}), headers=headers)

# Print the response
>>> resp.status_code
204
>>> resp.content
b''

        

Success: We see our 204 - No Content status code, and we expect no content to be returned from this method.

DELETEing a Row

Good news - the request and responses are identical for the DELETE HTTP Method:

resp = requests.delete("http://localhost:5000/api/programming_languages/1")
>>> resp.status_code
204
>>> resp.content
b''

Conclusion

That wraps up our walkthrough on how to upgrade from Flask-restless v0.17 to Flask-restless v1.0.0.

This only scratches the surface on great new features (and breaking changes) of Flask-restless v1.0.0. There are many other topics to cover, including: Resource Object Relationships, and how we make queries that handle Many-to-one, One-to-Many, and One-to-one relationships.