Colander i Django: ColanderSchemaField

Tym razem przedstawię rozwiązanie odwrotne do poprzedniego - zastosuję walidację schematem Colandera w formularzu Django.

Budując web API bardzo często istnieje potrzeba zwalidowania i odczytania nieco bardziej skomplikowanej struktury. W przypadku gdy po stronie serwera modelem jest obiekt klasy django.db.Model, to zapewne stosowany jest formularz klasy django.forms.ModelForm. To częsta i dosyć wygodna praktyka. Jednak jeśli wejście jest bardziej złożone, a dzieje się tak często podczas enkapsulacji interfejsu za pomocą RESTful API, to istnieje tendencja do tworzenia wielu formularzy, adapterów typu complex/composite forms, albo do zmiany modelu w celu odzwierciedlenia go w interfejsie 1:1.

Żadne z tych rozwiązań nie jest wystarczająco dobre, ponieważ wiele formularzy to bardziej złożona logika walidacji i brak spójności tejże (także w kontekście komunikatów o błędach), composite/complex forms narzucają tworzenie zlepek z wielu modeli (co też nie zawsze jest konieczne), a zmiany modelu mogą być zbędne i kosztowne.

Przypuśćmy, że bardziej złożona struktura to dodatkowy poziom zagnieżdżenia danych wejściowych. Naturalnym krokiem, szczególnie przy budowaniu JSON API, jest dodanie pola typu JSONField, ktore zdeserializuje część JSON-a do słownika. Byłoby to rozwiązanie dobre gdyby nie fakt, że walidacji podlega tylko syntaktyka przekazanej wartości. Nazwy kluczy, czyli ich prawidłowość oraz ich typy i wartości, pozostają nieweryfikowalne. I nie jest to winą JSON-a, tylko jego użycia (może wynikać wprost z zastosowania prostego pola typu JSONField).

Problem można rozwiązać używając pakietu Colander, za pomocą którego definiuje się dowolne schematy. Schematy Colander mogą być komponowane w złożone struktury, ale mogą być też używane pojedynczo. Colander nie narzuca tutaj żadnej konkretnej struktury oraz nie wprowadza zbędnych zależności, zatem użycie go jest bardzo wygodne.

Rozważmy bardzo prosty schemat opisujący współrzędne geograficzne:

class LocationSchema(colander.MappingSchema):
    lat = colander.SchemaNode(colander.Float())
    lng = colander.SchemaNode(colander.Float())

Powyższy schemat przyjmie słownik z kluczami lat oraz lng, których wartości muszą być liczbami zmiennoprzecinkowymi. Krótko mówiąc przyjmie on dane postaci: {"lat": 50.0, "lng": 50.0}.

Taki schemat można podpiąć bezpośrednio pod formularz Django, tak jak każde inne pole:

class LocationForm(forms.ModelForm):
    destination = ColanderSchemaField(LocationSchema, required=True)

Oczywiście potrzebny jest adapter - dedykowane pole dla formularzy Django, za pomocą którego proces walidacji zainstancjonuje schemat LocationSchema, zweryfikuje oraz oczyści dane wejściowe.

Ciało tego adaptera jest bardzo proste:

class ColanderSchemaField(forms.Field):
    def __init__(self, schema_factory, *args, **kwargs):
        self.schema_factory = schema_factory
        super(ColanderSchemaField, self).__init__(*args, **kwargs)

    def clean(self, value):
        if value is None:
            return

        schema = self.schema_factory()
        try:
            return schema.deserialize(value)
        except colander.Invalid as ex:
            raise forms.ValidationError(ex)

Od tej chwili formularz będzie oczekiwał klucza destination o strukturze określonej klasą LocationSchema.

Jednak zapis tej informacji oczywiście nie nastąpi, ponieważ ColanderSchemaField nie jest częścią modelu. Zapis trzeba przeprowadzić jawnie:

class LocationForm(forms.ModelForm):
    # ...

    def save(self, commit=True):
        instance = super(LocationForm, self).save(commit=False)
        destination = self.cleaned_data['destination']

        instance.destination_lat = destination['lat']
        instance.destination_lng = destination['lng']

        if commit:
            instance.save()
        return instance

Jeśli model miałby zdefiniowane pole destination klasy django.contrib.postgres.fields.JSONField lub analogicznej, to aktualizacja za pomocą ColanderSchemaField powinna przebiec automatycznie bez konieczności nadpisywania metody save(). Niestety jeszcze nie testowałem tego wariantu, do czego gorąco zachęcam.

Krótko o mnie

Jestem programistą i architektem systemów IT. Specjalizuję się w aplikacjach intra- oraz internetowyh. Zajmuję się wsparciem istniejących systemów oraz projektowaniem i produkcją.

Zainteresowanych moimi usługami zapraszam do wysłania zapytania.

Javascript logo PostgreSQL logo Cassandra logo Redis logo ElasticSearch logo Ansible logo HTML5 logo CSS3 logo NGINX logo Docker logo

Komentarze

Brak komentarzy