Metoda PATCH w HTTP mówi nam tyle, że do API należy wysłać opis zmian w zasobie. Jak to zrobić? Najlepiej i najwygodniej użyć formatu JSON Patch.
Na początek stwórzmy sobie dokument JSON. Będziemy na nim wykonywać zmiany. Niech to będzie JSON opisujący nasze ulubione danie:
{ "pizza" : { "size": "30cm", "thickness": "thick", "toppings": [ "Olives", "Mozarella", "Mushrooms", "Tomatoes", "Onions", "Basil" ] }, "extra": ["ketchup"] }
JSON Pointer czyli dopasujmy ścieżkę
JSON Pointer (opisany w RFC 6901) to sposób na wyszukiwanie elementów w dokumentach JSON. Przypomina XML-owe XPath. W bardzo prosty sposób możemy wyłuskać fragmenty dokumentu.
Kolejne elementy ścieżki dzielimy slashami. Elementy tablic oznaczamy liczbami począwszy od zera. Przykładowo:
"/pizza/size" - "30cm" "/pizza/toppings/1" - "mozarella" (drugi element, liczymy od zera) "/extra/0" - "ketchup" "/pizza/toppings" - zwraca nam całą tablicę dodatków
Ale jak dopasować się do pozycji zawierającej slasha? Bardzo prosto. Mamy dwie specjalne wartości oznaczane tyldą:
"~1" oznacza slash. Np. "a~1b" dopasuje się do klucza "a/b" "~0" oznacza tyldę. Np. "a~0b" dopasuje się do "a~b"
Teraz możemy łatwo definiować ścieżki do dokumentów JSON.
JSON Patch czyli modyfikacje JSONa
Standard JSON Patch mówi nam jak zmienić dokument JSON. Został stworzony specjalnie dla metody HTTP PATCH. Dokument patchujący definiuje serię zmian. Patch jest udany gdy wszystkie zmiany zostaną wprowadzone. Jeśli choć jedna się nie uda, dokument ma zostać niezmieniony. Nie można zrobić patcha, który tylko częściowo się uda.
Patchowanie korzysta z w/w ścieżek. Tak wygląda struktura JSON Patch:
[ {"op": "add", "path": "/pizza/toppings/-", ... }, {"op": "remove", "path": "/pizza/toppings/3", ... }, ... ]
Mamy tablicę operacji. Każda operacja zawiera nazwę, ścieżkę oraz opcjonalne dane.
Dodawanie („add”) wstawia nowy element lub podmienia istniejący. Możemy dodać nowy element do obiektu, wtedy podajemy ścieżkę do klucza. Możemy też wstawić coś do tablicy. Przykładowo:
[ // dodajemy paprykę do pizzy (na koniec tablicy) {"op":"add", "path":"/pizza/toppings/-", "value":"Pepper"}, // nowy element - wypieczona na chrupiąco {"op":"add", "path":"/pizza/finish", "value":"Crispy"}, // zmieniamy grubość na cienką {"op":"add", "path":"/pizza/thickness","value":"Thin"} ]
Usuwanie („remove”) jest jeszcze prostsze. Wystarczy podać operację i ścieżkę:
[ // usuwamy cebulę {"op":"remove", "path":"/pizza/toppings/4"} ]
Przenoszenie („move”) elementów też jest możliwe. Działa tak jakbyśmy usunęli element z jednej ścieżki i dodali w innej. Tutaj path oznacza ścieżkę docelową:
[ // przenosimy keczup na pizzę {"op":"move", "from":"/extra", "path":"/pizza/extra"} ]
Kopiowanie („copy”) jest tak proste jak przenoszenie:
[ // keczup, wszędzie keczup, na pizzy, z pizzą {"op":"copy", "from":"/extra", "path":"/pizza/extra"} ]
Zastąpienie („replace”) jednej wartości inną. Zastąpienie różni się od dodawania. Jeśli ścieżka wcześniej nie istniała, dodawanie utworzy dane. Zastąpienie zwróci błąd i cała seria zmian nie będzie naniesiona.
[ // powiększamy pizzę {"op":"replace", "path":"/pizza/size", "value":"31.4cm"} ]
Instrukcja warunkowa („test”) pozwala nam zmieniać dokument tylko, gdy dane są takie, jakie chcemy. Jeśli test nie będzie spełniony, cała modyfikacja się nie wykona. Przykładowo chcemy zamienić cebulę na ostrą papryczkę:
[ // sprawdzamy czy faktycznie na pozycji 4 jest cebula {"op":"test", "path":"/pizza/toppings/4", "value":"Onions"}, // zamieniamy cebulę {"op":"replace", "path":"/pizza/toppings/4", "value":"Jalapeno Peppers"} ]
Pamiętajmy, że lista zmian jest operacją atomową. Błąd w jednej z operacji powoduje nie wykonanie się żadnej z modyfikacji na liście.
W powyższych przykładach używam komentarzy w stylu C. Nie wszystkie parsery JSON pozwalają na komentarze.
Każdy przykład pracuje na pierwotnym dokumencie z pizzą. Gdybyśmy złączyli je w jedną listę operacji – nic by się nie wykonało. Pojawiłyby się błędy gdyż np. najpierw usunęliśmy cebulę a potem testujemy czy jest na liście.
Na koniec
We wpisie o metodach HTTP wspomniałem, że PATCH powinien zawierać listę modyfikacji. JSON Patch jest formatem tych zmian.
Warto przeczytać RFC 6901 definiujące JSON Pointer oraz kolejne RFC 6902 mówiące o JSON Patch. Standard jest prosty i łatwy. Możemy z powodzeniem używać go w serwisach REST.
Możemy też znaleźć ciekawy artykuł właśnie o poprawnym używaniu metody PATCH w HTTP.