Pedro's dev blog

Entendeno Serializers no Django: A Ponte Entre Seus Modelos e a API Moderna Parte 1

Published on
Published on
/10 mins read/---

Tutorial 1: Serialização - Django REST Framework

Introdução

Este tutorial abordará a criação de uma API Web simples para destacar código, semelhante a um "pastebin". Ao longo do caminho, ele apresentará os vários componentes que compõem o REST framework e fornecerá uma compreensão abrangente de como tudo se encaixa.

O tutorial é bastante detalhado, então você provavelmente deveria pegar um biscoito e uma xícara de sua bebida favorita antes de começar. Se você quiser apenas uma visão geral rápida, consulte a documentação de início rápido (substitua link_para_quickstart pelo link real).

Nota: O código para este tutorial está disponível no repositório encode/rest-framework-tutorial no GitHub. Sinta-se à vontade para clonar o repositório e ver o código em ação.

Configurando um Novo Ambiente

Antes de mais nada, criaremos um novo ambiente virtual usando venv. Isso garantirá que a configuração do nosso pacote seja mantida isolada de qualquer outro projeto em que estejamos trabalhando.

python3 -m venv env
source env/bin/activate

Agora que estamos dentro de um ambiente virtual, podemos instalar os requisitos do nosso pacote.

pip install django
pip install djangorestframework
pip install pygments  # Usaremos isso para o destaque de código

Nota: Para sair do ambiente virtual a qualquer momento, basta digitar deactivate. Para mais informações, consulte a documentação do venv.

Começando

Ok, estamos prontos para começar a codificar. Para começar, vamos criar um novo projeto para trabalhar.

cd ~
django-admin startproject tutorial
cd tutorial

Feito isso, podemos criar um aplicativo que usaremos para criar uma API Web simples.

python manage.py startapp snippets

Precisaremos adicionar nosso novo aplicativo snippets e o aplicativo rest_framework a INSTALLED_APPS. Vamos editar o arquivo tutorial/settings.py:

INSTALLED_APPS = [
    ...
    'rest_framework',
    'snippets',
]

Ok, estamos prontos para prosseguir.

Criando um Modelo para Trabalhar

Para os fins deste tutorial, começaremos criando um modelo Snippet simples que é usado para armazenar trechos de código. Vá em frente e edite o arquivo snippets/models.py. Nota: Boas práticas de programação incluem comentários. Embora você os encontre em nossa versão do repositório deste código tutorial, nós os omitimos aqui para focar no código em si.

from django.db import models
from pygments.lexers import get_all_lexers
from pygments.styles import get_all_styles
 
LEXERS = [item for item in get_all_lexers() if item[1]]
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
STYLE_CHOICES = sorted([(item, item) for item in get_all_styles()])
 
 
class Snippet(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    title = models.CharField(max_length=100, blank=True, default='')
    code = models.TextField()
    linenos = models.BooleanField(default=False)
    language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
    style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
 
    class Meta:
        ordering = ['created']

Também precisaremos criar uma migração inicial para nosso modelo de snippet e sincronizar o banco de dados pela primeira vez.

python manage.py makemigrations snippets
python manage.py migrate snippets

Criando uma Classe Serializer

A primeira coisa que precisamos para começar nossa API Web é fornecer uma maneira de serializar e desserializar as instâncias de snippet em representações como json. Podemos fazer isso declarando serializadores que funcionam de forma muito semelhante aos formulários do Django. Crie um arquivo no diretório snippets chamado serializers.py e adicione o seguinte.

from rest_framework import serializers
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES
 
 
class SnippetSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    title = serializers.CharField(required=False, allow_blank=True, max_length=100)
    code = serializers.CharField(style={'base_template': 'textarea.html'})
    linenos = serializers.BooleanField(required=False)
    language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default='python')
    style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly')
 
    def create(self, validated_data):
        """
        Cria e retorna uma nova instância `Snippet`, dados os dados validados.
        """
        return Snippet.objects.create(**validated_data)
 
    def update(self, instance, validated_data):
        """
        Atualiza e retorna uma instância `Snippet` existente, dados os dados validados.
        """
        instance.title = validated_data.get('title', instance.title)
        instance.code = validated_data.get('code', instance.code)
        instance.linenos = validated_data.get('linenos', instance.linenos)
        instance.language = validated_data.get('language', instance.language)
        instance.style = validated_data.get('style', instance.style)
        instance.save()
        return instance

A primeira parte da classe serializer define os campos que são serializados/desserializados. Os métodos create() e update() definem como instâncias completas são criadas ou modificadas ao chamar serializer.save().

Uma classe serializer é muito semelhante a uma classe Form do Django e inclui flags de validação semelhantes nos vários campos, como required, max_length e default.

As flags de campo também podem controlar como o serializador deve ser exibido em determinadas circunstâncias, como ao renderizar para HTML. A flag {'base_template': 'textarea.html'} acima é equivalente a usar widget=widgets.Textarea em uma classe Form do Django. Isso é particularmente útil para controlar como a API navegável deve ser exibida, como veremos mais adiante no tutorial.

Na verdade, também podemos economizar algum tempo usando a classe ModelSerializer, como veremos mais tarde, mas por enquanto manteremos nossa definição de serializador explícita.

Trabalhando com Serializers

Antes de prosseguirmos, vamos nos familiarizar com o uso de nossa nova classe Serializer. Vamos entrar no shell do Django.

python manage.py shell

Ok, depois de fazer algumas importações, vamos criar alguns trechos de código para trabalhar.

from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from rest_framework.renderers import JSONRenderer
from rest_framework.parsers import JSONParser
 
snippet = Snippet(code='foo = "bar"\n')
snippet.save()
 
snippet = Snippet(code='print("hello, world")\n')
snippet.save()

Agora temos algumas instâncias de snippet para brincar. Vamos dar uma olhada na serialização de uma dessas instâncias.

serializer = SnippetSerializer(snippet)
serializer.data
# {'id': 2, 'title': '', 'code': 'print("hello, world")\n', 'linenos': False, 'language': 'python', 'style': 'friendly'}

Neste ponto, traduzimos a instância do modelo para tipos de dados nativos do Python. Para finalizar o processo de serialização, renderizamos os dados em json.

content = JSONRenderer().render(serializer.data)
content
# b'{"id":2,"title":"","code":"print(\\"hello, world\\")\\n","linenos":false,"language":"python","style":"friendly"}'

A desserialização é semelhante. Primeiro, analisamos um fluxo em tipos de dados nativos do Python...

import io
 
stream = io.BytesIO(content)
data = JSONParser().parse(stream)

...então restauramos esses tipos de dados nativos em uma instância de objeto totalmente preenchida.

serializer = SnippetSerializer(data=data)
serializer.is_valid()
# True
serializer.validated_data
# {'title': '', 'code': 'print("hello, world")', 'linenos': False, 'language': 'python', 'style': 'friendly'}
serializer.save()
# <Snippet: Snippet object>

Observe como a API é semelhante ao trabalho com formulários. A semelhança deve se tornar ainda mais aparente quando começarmos a escrever views que usam nosso serializador.

Também podemos serializar querysets em vez de instâncias de modelo. Para fazer isso, simplesmente adicionamos uma flag many=True aos argumentos do serializador.

serializer = SnippetSerializer(Snippet.objects.all(), many=True)
serializer.data
# [{'id': 1, 'title': '', 'code': 'foo = "bar"\n', 'linenos': False, 'language': 'python', 'style': 'friendly'}, {'id': 2, 'title': '', 'code': 'print("hello, world")\n', 'linenos': False, 'language': 'python', 'style': 'friendly'}, {'id': 3, 'title': '', 'code': 'print("hello, world")', 'linenos': False, 'language': 'python', 'style': 'friendly'}]

Usando ModelSerializers

Nossa classe SnippetSerializer está replicando muitas informações que também estão contidas no modelo Snippet. Seria bom se pudéssemos manter nosso código um pouco mais conciso.

Da mesma forma que o Django fornece classes Form e classes ModelForm, o REST framework inclui classes Serializer e classes ModelSerializer.

Vamos dar uma olhada na refatoração do nosso serializador usando a classe ModelSerializer. Abra o arquivo snippets/serializers.py novamente e substitua a classe SnippetSerializer pelo seguinte.

class SnippetSerializer(serializers.ModelSerializer):
    class Meta:
        model = Snippet
        fields = ['id', 'title', 'code', 'linenos', 'language', 'style']

Uma propriedade interessante que os serializadores têm é que você pode inspecionar todos os campos em uma instância de serializador, imprimindo sua representação. Abra o shell do Django com python manage.py shell e tente o seguinte:

from snippets.serializers import SnippetSerializer
serializer = SnippetSerializer()
print(repr(serializer))
# SnippetSerializer():
#    id = IntegerField(label='ID', read_only=True)
#    title = CharField(allow_blank=True, max_length=100, required=False)
#    code = CharField(style={'base_template': 'textarea.html'})
#    linenos = BooleanField(required=False)
#    language = ChoiceField(choices=[('Clipper', 'FoxPro'), ('Cucumber', 'Gherkin'), ('RobotFramework', 'RobotFramework'), ('abap', 'ABAP'), ('ada', 'Ada')...
#    style = ChoiceField(choices=[('autumn', 'autumn'), ('borland', 'borland'), ('bw', 'bw'), ('colorful', 'colorful')...

É importante lembrar que as classes ModelSerializer não fazem nada particularmente mágico, elas são simplesmente um atalho para criar classes de serializador:

  • Um conjunto de campos determinado automaticamente.
  • Implementações padrão simples para os métodos create() e update().

Escrevendo Views Regulares do Django Usando Nosso Serializer

Vamos ver como podemos escrever algumas views de API usando nossa nova classe Serializer. Por enquanto, não usaremos nenhum dos outros recursos do REST framework, apenas escreveremos as views como views regulares do Django.

Edite o arquivo snippets/views.py e adicione o seguinte.

from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.parsers import JSONParser
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
 
@csrf_exempt
def snippet_list(request):
    """
    Lista todos os trechos de código ou cria um novo trecho.
    """
    if request.method == 'GET':
        snippets = Snippet.objects.all()
        serializer = SnippetSerializer(snippets, many=True)
        return JsonResponse(serializer.data, safe=False)
 
    elif request.method == 'POST':
        data = JSONParser().parse(request)
        serializer = SnippetSerializer(data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data, status=201)
        return JsonResponse(serializer.errors, status=400)

A raiz da nossa API será uma view que suporta listar todos os snippets existentes ou criar um novo snippet.

Observe que, como queremos ser capazes de fazer POST para esta view a partir de clientes que não terão um token CSRF, precisamos marcar a view como csrf_exempt. Isso não é algo que você normalmente gostaria de fazer, e as views do REST framework realmente usam um comportamento mais sensato do que isso, mas servirá para nossos propósitos agora.

Também precisaremos de uma view que corresponda a um snippet individual e possa ser usada para recuperar, atualizar ou excluir o snippet.

@csrf_exempt
def snippet_detail(request, pk):
    """
    Recupera, atualiza ou exclui um trecho de código.
    """
    try:
        snippet = Snippet.objects.get(pk=pk)
    except Snippet.DoesNotExist:
        return HttpResponse(status=404)
 
    if request.method == 'GET':
        serializer = SnippetSerializer(snippet)
        return JsonResponse(serializer.data)
 
    elif request.method == 'PUT':
        data = JSONParser().parse(request)
        serializer = SnippetSerializer(snippet, data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data)
        return JsonResponse(serializer.errors, status=400)
 
    elif request.method == 'DELETE':
        snippet.delete()
        return HttpResponse(status=204)

Finalmente, precisamos conectar essas views. Crie o arquivo snippets/urls.py:

from django.urls import path
from snippets import views
 
urlpatterns = [
    path('snippets/', views.snippet_list),
    path('snippets/<int:pk>/', views.snippet_detail),
]

Também precisamos conectar o urlconf raiz, no arquivo tutorial/urls.py, para incluir os URLs do nosso aplicativo de snippet.

from django.urls import path, include
 
urlpatterns = [
    path('', include('snippets.urls')),
]

Vale a pena notar que existem alguns casos extremos com os quais não estamos lidando adequadamente no momento. Se enviarmos json malformado, ou se uma solicitação for feita com um método que a view não manipula, terminaremos com uma resposta 500 "erro do servidor". Ainda assim, isso servirá por enquanto.

Testando Nossa Primeira Tentativa de uma API Web

Agora podemos iniciar um servidor de exemplo que serve nossos snippets.

Saia do shell...

quit()

...e inicie o servidor de desenvolvimento do Django.

python manage.py runserver
 
Validating models...
 
0 errors found
Django version 5.0, using settings 'tutorial.settings'
Starting Development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Em outra janela de terminal, podemos testar o servidor.

Podemos testar nossa API usando curl ou httpie. Httpie é um cliente http amigável escrito em Python. Vamos instalar isso.

Você pode instalar o httpie usando o pip:

pip install httpie

Finalmente, podemos obter uma lista de todos os snippets:

http GET http://127.0.0.1:8000/snippets/ --unsorted
 
HTTP/1.1 200 OK
...
[
    {
        "id": 1,
        "title": "",
        "code": "foo = \"bar\"\n",
        "linenos": false,
        "language": "python",
        "style": "friendly"
    },
    {
        "id": 2,
        "title": "",
        "code": "print(\"hello, world\")\n",
        "linenos": false,
        "language": "python",
        "style": "friendly"
    },
    {
        "id": 3,
        "title": "",
        "code": "print(\"hello, world\")",
        "linenos": false,
        "language": "python",
        "style": "friendly"
    }
]

Ou podemos obter um snippet específico referenciando seu id:

http GET http://127.0.0.1:8000/snippets/2/ --unsorted
 
HTTP/1.1 200 OK
...
{
    "id": 2,
    "title": "",
    "code": "print(\"hello, world\")\n",
    "linenos": false,
    "language": "python",
    "style": "friendly"
}

Da mesma forma, você pode ter o mesmo json exibido visitando esses URLs em um navegador da web.

Onde Estamos Agora

Estamos indo bem até agora, temos uma API de serialização que se parece muito com a API de formulários do Django e algumas views regulares do Django.

Nossas views de API não fazem nada particularmente especial no momento, além de servir respostas json, e há alguns casos extremos de tratamento de erros que ainda gostaríamos de limpar, mas é uma API Web funcional.

Veremos como podemos começar a melhorar as coisas na parte 2 do tutorial.