Strona główna / Artykuły / Wykrywanie niezauważonej zmiany umowy API za pomocą typów wywnioskowanych z przykładów i Zod

Wykrywanie niezauważonej zmiany umowy API za pomocą typów wywnioskowanych z przykładów i Zod

Dlaczego ręcznie napisane typy TypeScript dla API third-party stają się przestarzałe, jak pomocne jest wywnioskowywanie typów i schematów Zod z rzeczywistych odpowiedzi, oraz w jaki sposób różnice między zrzutami obrazu ujawniają odchylenia.

1565 słów

API dostarczane przez strony trzecie mogą zmieniać swój format bez uprzedzenia, a TypeScript tego nie zauważy, ponieważ Twoje typy opisują wygląd odpowiedzi w momencie jej stworzenia, a nie ten obecny. Ten artykuł wyjaśnia, jak dochodzi do takich problemów, dlaczego generowanie typów na podstawie kilku rzeczywistych odpowiedzi jest lepsze od ich ręcznego wpisywania oraz dlaczego tym, co naprawdę Cię chroni, jest porównywanie nowych odpowiedzi z zapisanym ich obrazem. Zobaczysz również, gdzie ten podejście pasuje do narzędzi takich jak OpenAPI i testowanie kontraktów, a gdzie nie.

Jak pole o zmienionej nazwie może umknąć uwadze

Rozważmy interfejs użytkownika, który integruje się z dostawcą płatności. Pewnego dnia pole w jednej z odpowiedzi dostawcy zmienia się z user_id na userId. Nie ma żadnej notatki o zmianie, żadnego ogłoszenia ani podwyższenia wersji. Najprawdopodobniej inżynier po stronie dostawcy uporządkował niejednolite nazwy, ich zestaw testów przeszedł pomyślnie i zmiana została wdrożona.

Nic się nie zawala po stronie konsumenta, a to właśnie jest problem. Kod nadal odczytuje response.user_id, a TypeScript to akceptuje, ponieważ interfejs został ręcznie skonfigurowany miesiące temu na podstawie przykładu z Postman, który już nie odzwierciedla rzeczywistości. Ten interfejs nadal obiecuje pole user_id. W czasie wykonywania aplikacji wartość ta jest po prostu undefined. Przez dwa tygodnie trzy ścieżki kodu cicho zapisują undefined do pola zawierającego kwotę, aż w końcu zgłoszenie do obsługi klienta ujawnia błąd. Nie wyświetla się żadne ostrzeżenie, żaden proces kompilacji nie zmienia koloru na czerwony, a aplikacja nadal robi to źle bez żadnych skarg.

Zespoły pracujące z zewnętrznymi APIi przez dłuższy czas niemal zawsze napotykają jakąś wersję tego problemu.

Słaby punkt to źródło typów

TypeScript nie jest tu winny. Problem leży w pochodzeniu typów. Interfejsy zazwyczaj traktuje się tak, jakby pochodziły od czegoś autorytatywnego, takiego jak schemat, umowa lub jedyny źródło prawdy. W praktyce wiele z nich pochodzi z jednej przykładowej odpowiedzi, którą ktoś przepisał ręcznie. Ten interfejs jest następnie kopiowany do kilku innych plików i traktowany jako fakt, a nikt nie sprawdza go ponownie, dopóki coś się nie zepsuje.

Prawdziwą umową jest to, co API zwraca obecnie w produkcji. Znajduje się to na serwerze, którego nie kontrolujesz, i może ulec zmianie bez twojej zgody. Ręcznie napisane typy to zrzut ekranu z chwili, która już minęła, a kompilator nie ma sposobu, by o tym wiedzieć.

Istnieje tu również głębszy problem. Typy w TypeScript znikają podczas kompilacji, więc nigdy nic nie sprawdzają w czasie wykonywania. Jeśli zmieni się struktura danych, tylko walidacja w czasie wykonywania na granicy, na przykład analiza odpowiedzi za pomocą schematu Zod, zamienia ciche undefined na natychmiastową, widoczną błąd.

Wynikanie typów z kilku rzeczywistych odpowiedzi

Najbardziej pomocne w tym przypadku nie jest bardziej zaawansowane TypeScript ani sprytniejsze generyki, lecz mechaniczny proces: pobieranie odpowiedzi faktycznie zwróconych przez API, generowanie na ich podstawie typów oraz otrzymywanie powiadomień, gdy rzeczywistość przestaje odpowiadać tym typom.

Wartość wejściowa powinna stanowić rzeczywiste odpowiedzi w formacie JSON, a nie dokumentację ani schemat. Na ich podstawie narzędzie może wywnioskować zarówno typ w TypeScript, jak i odpowiedni schemat Zod. Użycie kilku przykładów ma większe znaczenie, niż mogłoby się wydawać. Jedna odpowiedź pokazuje, jak może wyglądać treść przesyłana w pakiecie. Trzy lub cztery odpowiedzi ujawniają, które pola są rzeczywiście opcjonalne, które czasami przyjmują wartość null, a które elementy tablicy mają niespójną strukturę. Jeden przykład zawsze wprowadza w błąd przez pominięcie informacji.

Poniższy przykład przedstawia dwie odpowiedzi dla tej samej zasoby – jedną z polem user_id, a drugą z polem userId – i pokazuje typ w TypeScript oraz schemat Zod wywnioskowane z obu:

// paste these two responses in...
[
  {
    "user_id": "pot_00009exampleP0tOxWb",
    "name": "Wedding Fund",
    "balance": 550100,
    "currency": "GBP",
    "created": "2025-11-09T12:30:53.695Z",
    "updated": "2025-02-26T07:12:04.925Z"
  },
  {
    "userId": "pot_00009exampleP0tOxWb",
    "name": "Wedding Fund",
    "balance": 550,
    "currency": "EUR",
    "created": "2025-11-09T12:30:53.695Z",
    "updated": "2025-03-26T07:12:04.925Z"
  }
]

// ...get this out typescript
type Root = {
  user_id?: string
  name: string
  balance: number
  currency: string
  created: string
  updated: string
  userId?: string
}[]

// or ... get this out zod
import { z } from 'zod'

const Root = z.array(z.object({
  user_id: z.string().optional(),
  name: z.string(),
  balance: z.number(),
  currency: z.string(),
  created: z.string(),
  updated: z.string(),
  userId: z.string().optional(),
}))

Przyjrzyj się uważnie temu, co pokazuje połączony wynik. Ponieważ każda nazwa występuje tylko w jednym przykładzie, zarówno user_id, jak i userId stają się opcjonalne. Jest to technicznie dokładne, ale jednocześnie ukrywa zmianę nazwy: kod, który odczytuje któreś z tych pól, nadal sprawdza typ, a odpowiedź nie zawierająca żadnego z nich również przejdzie weryfikację schematu Zod. Przykłady wskazują również na problem, którego typowanie nigdy nie może wykryć: balance spada z 550100 na 550, podczas gdy zmienia się currency, co może oznaczać przejście między mniejszymi a większymi jednostkami waluty. W obu przypadkach typem, który zostaje wywnioskowany, jest number. Typowanie podpowiada kształt danych, ale nie może wyjaśnić ich znaczenia.

Wiele generatorów kodu zatrzymuje się w tym momencie. Przejście od danych bez typów do danych z typami jest przydatne, ale nie rozwiązuje problemu zmian w trakcie działania aplikacji.

Zrzuty stanu i różnice pokazują zmiany

Najważniejszy krok następuje po wygenerowaniu treści. Gdy typy zostaną wydobyte z rzeczywistej odpowiedzi, tę odpowiedź można zapisać jako zdjęcie stanu. Za każdym razem, gdy pobierasz nowy przykład z tego samego punktu końcowego, porównujesz go ze zdjęciem stanu i otrzymujesz dokładny raport o tym, co się zmieniło – czy to pole o nowej nazwie, wartość, która teraz może być pusta, podczas gdy wcześniej była zwykłą string, czy dodatkowy klucz pojawiający się w zagnieżdżonym obiekcie. Zamiast niejasnego komunikatu „coś poszło nie tak”, widzisz dokładną różnicę w strukturze.

To właśnie to porównanie odróżnia generator typów od detektora odchyłek. Generowanie kodu pozwala stworzyć kod typowany z niczego. Detekcja odchyłek zapobiega temu, by zmiana nazwy z user_id na userId pozostała niezauważona w środowisku produkcyjnym przez tygodnie. W powyższym przykładzie różnica między zrzutami stanu pokazałaby „user_id usunięty, userId dodany”, zamiast po cichu zmieniać oba pola na opcjonalne.

Zachowuj dane z środowiska produkcyjnego na swoim komputerze

Aby wykryć rzeczywisty drift, potrzebne są prawdziwe dane – syntetyczne obciążenia nie ujawnią zmian, które Cię interesują. Dlatego ochrona prywatności stanowi wymóg projektowy. Odpowiedzi z produkcji mogą zawierać informacje o klientach, więc wklejanie ich do formularza internetowego, który przesyła je na serwer third-party, stwarza nowe ryzyko dotyczące przetwarzania danych. Narzędzia do tego celu powinny działać lokalnie, na przykład wyłącznie w karcie przeglądarki lub jako skrypt w Twoim własnym repozytorium, aby obciążenia nigdy nie opuszczały Twojego środowiska.

Gdzie ta metoda się sprawdza, a gdzie nie

Inferencja oparta na próbkach z wykrywaniem driftu nie zastępuje OpenAPI ani rozwiązań do testowania kontraktów, takich jak Pact. Jeśli posiadasz zarówno dostawcę, jak i odbiorcę oraz możesz narzucić schemat na poziomie źródła, rób to – to lepsze rozwiązanie długoterminowe.

Ta technika jest skierowana na częściej występującą i mniej atrakcyjną sytuację: korzystasz z API, którego nie kontrolujesz, dokumentacja jest przestarzała lub brakuje jej, a generowanie klienta na podstawie specyfikacji OpenAPI nie jest możliwe, ponieważ takiej specyfikacji nie ma lub nikt jej nie ufa. Opisuje to większość integracji z procesorami płatności, usługami wewnętrznymi należącymi do innych zespołów oraz API dostawców zewnętrznych. W takich warunkach rzeczywista odpowiedź jest jedynym dostępnym źródłem prawdy, więc to od niej powinny być wyprowadzane twoje typy.

Nie rozszerzaj zbytnio zakresu. JSON włączone, TypeScript i Zod wykluczone, plus detekcja odchyleń – to wystarczy do zaspokojenia potrzeb. Próba obsługi XML, protobuf oraz każdego możliwego przypadku krawędziowego schematu zamienia skuteczne narzędzie w niewyraźne. Aby uzyskać szerszy przegląd decyzji dotyczących kontraktów, które mogą zaszkodzić interfejsom użytkownika, sprawdź powszechne błędy w kontraktach API, które niszczą niezawodność interfejsów użytkownika.

Pierwszy praktyczny test

Najlepszym miejscem do przetestowania tego rozwiązania jest integracja, która już doświadczyła bezgłośnej zmiany struktury. Weź stare odpowiedzi oraz najnowsze z tego samego endpointu, przeprowadź na nich analizę i porównanie w formie „snapshot”, a następnie sprawdź, co zostanie zaznaczone jako problematyczne. Widok rzeczywistej historycznej zmiany w porównaniu jest bardziej przekonujący niż jakikolwiek argument na rzecz tego podejścia.

Główne wnioski

  • Ręcznie pisane interfejsy dla API third-party to zrzuty stanu z przeszłości, a TypeScript nie potrafi określić, kiedy stają się przestarzałe.
  • Wynoś typy oraz schematy Zod na podstawie kilku rzeczywistych odpowiedzi, ponieważ wiele przykładów ujawnia pola opcjonalne, mogące być null oraz niespójne, które jeden przykład ukrywa.
  • Waliduj odpowiedzi w czasie wykonywania programu, na granicy API, aby zmiany struktury powodowały wyraźne błędy zamiast generowania wartości undefined.
  • Zachowuj odpowiedzi jako zrzuty stanu i porównuj nowe przykłady z nimi; sama inferencja po połączeniu może zamaskować zmianę nazwy na dwie pola opcjonalne.
  • Zachowuj dane używane w produkcji lokalnie i preferuj testy OpenAPI lub kontraktowe, gdy kontrolujesz obie strony API.

Literatura pokrewna

  • Ważny JSON, złamany kontrakt: warstwowe sprawdzenia dla regresji ładunku — Dowiedz się, jak wykrywać regresje w ładunku JSON, który jest poprawnie parsowany: różnice semantyczne, skoncentrowane JSON Schema, twierdzenia dotyczące reguł biznesowych w Node oraz ograniczenia generowanych typów.
  • Metoda HTTP QUERY dla zespołów frontendu: bezpieczne odczyty z ciałem — Dowiedz się, kiedy metoda HTTP QUERY jest lepsza od GET i POST przy złożonych filtrach, jak używać jej z fetch oraz jakie wymagania stawiają CORS, cacheowanie i wsparcie infrastruktury.