API się zmienia, urządzenia zostają: kompatybilność przez kilka lat
Jak dodać jakość i brak pomiaru bez cichej zmiany znaczenia danych? Kontrakty JSON i proto3, macierz wersji, migracja, historia i rollback.

Czujnik od dwóch lat wysyła temperaturę jako liczbę. Nowy firmware ma odróżniać poprawny pomiar od wartości nieaktualnej oraz informować, że aktualnego wyniku w ogóle nie ma. Zespół dodaje pole quality, aktualizuje serwer i uznaje zmianę za zgodną wstecznie. Przecież stara liczba nadal jest w komunikacie.
Problem pojawia się, gdy sensor przestaje odpowiadać. Nowy kod zachowuje ostatnią temperaturę i ustawia quality: "stale". Stary odbiorca ignoruje nieznane pole, widzi znajomą liczbę i nadal prezentuje ją jako aktualną. Parser działa, połączenie jest poprawne, alarmu błędu API nie ma. Mimo to system przekazuje inną informację, niż zakładał użytkownik.
Kompatybilność trzeba więc rozpatrywać przez zachowanie całej ścieżki danych. W tym artykule rozwijamy jeden fikcyjny kontrakt pomiarowy, przygotowujemy macierz współpracy wersji i plan migracji z możliwością wycofania wdrożenia. Przykłady służą do przeglądu projektu; nie są gotową usługą ani deklaracją zgodności konkretnej floty.
Co obiecywała stara liczba
Zacznijmy od zapisania starego kontraktu v1. Urządzenie wysyła komunikat tylko wtedy, gdy wykonało pomiar, który jego dotychczasowa diagnostyka uznaje za dostępny. Temperatura jest skończoną liczbą w stopniach Celsjusza. measuredAt opisuje chwilę pomiaru, nie chwilę wysłania, zapisania ani odczytu. Przykładowa wiadomość wygląda tak:
{
"temperature": 21.5,
"measuredAt": "2026-10-01T12:00:00Z"
}
Brak nowego komunikatu nie oznacza zera stopni. Odbiorca powinien mieć osobną regułę aktualności. W naszym przykładzie do nowej funkcji wymagającej bieżącego pomiaru dopuszczamy wiek od zera do 60 sekund włącznie. To założenie aplikacji, nie ogólna cecha pomiaru temperatury. W innym procesie właściwe mogą być milisekundy lub godziny.
Stary format nie mówi nic o jakości według nowych kryteriów. Wiemy, że stary firmware dopuścił wysłanie wyniku zgodnie ze swoim zachowaniem. Nie wiemy, czy wykonał później dodany test, czy potrafił wykryć nowy rodzaj uszkodzenia. Adapter nie powinien zmieniać tego braku wiedzy w deklarację valid. Dla takich danych stosujemy wewnętrzną wartość legacy_unknown.
Ta decyzja ma konsekwencję produktową. Nowa funkcja zależna od potwierdzonej jakości może być niedostępna dla starego urządzenia, chociaż jego dotychczasowy widok temperatury działa dalej. To jawne ograniczenie możliwości. Udawanie pełnej jakości utrzymałoby pozorną jednolitość interfejsu kosztem wiarygodności danych.
Trzy różne pytania o zgodność
Google AIP-180 rozróżnia zgodność kodu źródłowego, komunikacji i znaczenia. W praktyce przeglądu przydaje się prosty test: czy klient się kompiluje, czy rozumie wiadomość i czy po jej odczytaniu podejmuje tę samą uzasadnioną decyzję? Spełnienie pierwszych dwóch warunków nie gwarantuje trzeciego.
Zmiana nazwy pola może być usunięciem starego i dodaniem nowego z perspektywy odbiorcy. Dodanie wartości do wyliczenia może być poprawne dla formatu, ale zaskoczyć kod z zamkniętą listą stanów. Zmiana jednostki z Celsjusza na kelwiny pozostawia poprawną liczbę, lecz całkowicie zmienia jej interpretację. Dlatego test „odpowiedź ma status 200 i da się odczytać jako JSON” sprawdza bardzo małą część kontraktu.
Nie należy też mówić ogólnie, że „JSON ignoruje dodatkowe pola”. Parser JSON odczytuje strukturę. Późniejszy walidator może odrzucać nieznane klucze, a aplikacja może je zachować, pominąć lub błędnie przetworzyć. W naszym demonstratorze S1 celowo dopuszcza dokładnie dwa klucze v1. Dodanie quality bez uzgodnienia zmiany powoduje odrzucenie, mimo że wiadomość jest składniowo poprawna.
Nowy format musi wyrazić brak wyniku
Wybieramy osobny kontrakt v2, kierowany do odbiorcy obsługującego nowy schemat. Nazwa i jednostka są jawne, a jakość odnosi się do konkretnej próbki. Poprawny wynik ma postać:
{
"schema": 2,
"temperature": {
"value": 21.5,
"unit": "Cel",
"quality": "valid",
"measuredAt": "2026-10-01T12:00:00Z"
}
}
W tym przykładzie Cel jest umówioną wartością jednostki. Kontrakt nie przyjmuje dowolnego napisu i nie zakłada, że każdy odbiorca sam wykona konwersję. Wartość valid oznacza przejście diagnostyki zadeklarowanej dla danego wydania urządzenia. Nie oznacza bezbłędności pomiaru ani zastąpienia budżetu niepewności.
Brak wyniku reprezentujemy inaczej:
{
"schema": 2,
"temperature": {
"value": null,
"unit": "Cel",
"quality": "missing",
"measuredAt": null
}
}
Ustalamy regułę spójności: missing wymaga obu wartości null. valid i stale wymagają liczby oraz czasu pomiaru. stale może zachować historyczną wartość, ale nie jest dopuszczony do działania wymagającego aktualnej próbki. Przyszła, nieznana jakość zachowuje się zachowawczo: odbiorca nie awansuje jej do valid i sygnalizuje brak obsługi znaczenia.
Nie używamy zera, wartości minus 999 ani pustego napisu jako braku wyniku. Każda z tych reprezentacji może zostać potraktowana jak pomiar albo zamaskować błąd walidacji. NaN i nieskończoność nie są liczbami dopuszczonymi w gramatyce JSON opisanej w RFC 8259. Umieszczenie napisu "NaN" tworzy napis, nie specjalną temperaturę. Znaczenie braku musi wynikać z kontraktu.
Macierz opisuje zachowanie, nie same numery
Wprowadzamy dwa wydania firmware: F1 zna tylko v1, F2 zna v2 i kontrolowany tryb emisji v1. Usługa S1 obsługuje wyłącznie stary kontrakt. S2 potrafi odczytać oba i zapisać informację o pochodzeniu. Nie zakładamy, że zmiana serwera automatycznie aktualizuje urządzenia albo wszystkie aplikacje użytkowników.
F1 z S2 nadal przekazuje temperaturę i czas. S2 oznacza jakość jako nieznaną według nowego modelu. F2 z S1 może działać tylko w trybie v1, którego emisja zachowuje stare obietnice. Jeśli F2 ma missing, stale lub nieznany stan, nie generuje sztucznej liczby. W tym trybie nie emituje nowego pomiaru, a stary odbiorca dochodzi do braku aktualności przez istniejący mechanizm czasu.
Ten wariant jest dopuszczalny tylko wtedy, gdy taki mechanizm rzeczywiście istnieje i działa. Jeżeli stary klient bezterminowo wyświetla ostatnią wartość jako bieżącą, adapter nie naprawi jego zachowania. Trzeba zaktualizować klienta, ograniczyć funkcję albo wyłączyć niebezpieczną kombinację. Nie ma bezstratnego sposobu przedstawienia wszystkich stanów v2 w liczbie, która nie miała takiej semantyki.
Bezpośrednie wysłanie v2 do S1 kończy się jawnym odrzuceniem. Próba „sprytnego” rozpoznania, że obiekt temperatury pewnie zawiera liczbę w którymś polu, utrudniłaby kontrolę kontraktu. Nowa usługa nie powinna też zgadywać wersji na podstawie obecności przypadkowego klucza, jeśli istnieje jawna wersja kanału lub komunikatu.
Próbki, które wykrywają rzeczywiste błędy
Zestaw kontraktowy powinien zawierać konkretne oczekiwane decyzje. Ustalmy czas oceny na 12:00:30Z, a czas próbki na 12:00:00Z tego samego dnia. Wiek wynosi 30 sekund. Dla v2 z valid i temperaturą 21,5 wynik może zasilić nową funkcję. Dla identycznej liczby ze stanem stale nie może. Dla v1 liczba pozostaje dostępna w starym znaczeniu, lecz nie uzyskuje automatycznie nowego poświadczenia jakości.
Sprawdzamy także dokładną granicę: 60 sekund jest dopuszczone, 60,001 już nie. Próbka z czasem przyszłym nie jest „bardzo świeża”; w tym uproszczonym przykładzie blokuje użycie do nowej funkcji. W produkcji trzeba zdefiniować tolerancję synchronizacji zegara i sposób postępowania przy jej przekroczeniu. Nie wolno ukryć tego problemu przez obcięcie ujemnego wieku do zera.
Kolejne przypadki obejmują zero stopni jako prawdziwy pomiar, null jako brak, nieznany stan warming_up, niewłaściwą jednostkę oraz sprzeczne missing z liczbą. W JavaScript Number(null) daje zero. Dlatego walidacja nie może zaczynać się od ogólnej konwersji typu. Najpierw ustalamy, czy wartość jest liczbą o dopuszczonym znaczeniu, dopiero później wykonujemy obliczenia.
Opisane reguły sprawdzono na niewielkim modelu kontraktu ze świadomie ograniczonym formatem czasu UTC. Nie jest to kompletna biblioteka walidacji dat ani implementacja sieciowa. Testy porównują oczekiwane zachowania i odrzucenia; nie sprowadzają zgodności do zgodności typów.
Protobuf nie usuwa problemu znaczenia
Gdy transport używa binarnego Protocol Buffers, obowiązują inne zasady ewolucji niż dla naszego ręcznie opisanego JSON. Przewodnik proto3 opisuje dodawanie pól jako bezpieczne dla formatu binarnego, przy konieczności poprawnej obsługi wartości domyślnych. Numeru istniejącego pola nie zmieniamy i nie przydzielamy usuniętego numeru nowemu znaczeniu. Bezpieczeństwo zapisu nie dowodzi poprawności logiki aplikacji.
Dla nowego, osobno identyfikowanego komunikatu można zapisać intencję w taki sposób:
syntax = "proto3";
message TemperatureV2 {
optional double value_celsius = 1;
Quality quality = 2;
string measured_at = 3;
enum Quality {
QUALITY_UNSPECIFIED = 0;
QUALITY_VALID = 1;
QUALITY_STALE = 2;
QUALITY_MISSING = 3;
}
}
To nowy typ, nie instrukcja zastąpienia starego pola o numerze 1 innym typem w istniejącym komunikacie. optional pozwala rozróżnić obecne zero od nieobecnej wartości. Domyślny stan jakości jest nieokreślony. Walidacja nadal musi sprawdzić spójność: QUALITY_VALID wymaga obecnej liczby i czasu, a QUALITY_MISSING wymaga braku liczby oraz pustego czasu według tego przykładowego kontraktu.
Dokumentacja obecności pól wyjaśnia różnicę między domyślną wartością skalarną a jawną obecnością. Nie zakładamy jednak, że dodanie optional naprawi wszystkie stare pośredniki: odczyt i ponowny zapis przez kod bez śledzenia obecności może zgubić informację o jawnie ustawionym zerze. Trzeba sprawdzić pełną trasę danych, również bramkę i eksport.
Osobna pułapka to ProtoJSON. Jego reguł nie należy utożsamiać z binarnym transportem Protobuf ani z dowolnym obiektem JSON. Nieznane pola są domyślnie odrzucane, a ich zachowanie nie jest przenoszone tak jak w formacie binarnym. Przejście przez konwerter może więc zmienić kompatybilność. Przykładowy null z naszego własnego v2 nie jest deklaracją identycznej reprezentacji ProtoJSON powyższego typu.
Numer firmware nie jest negocjacją możliwości
Wersja oprogramowania urządzenia i wersja kontraktu odpowiadają na różne pytania. Firmware może dostać poprawkę sterownika bez zmiany wiadomości. Ten sam format może też być wysyłany przez kilka rewizji sprzętu. Jeśli serwer rozpoznaje możliwości wyłącznie po warunku „wersja większa niż pewien numer”, łatwo pomyli wydanie specjalne, powrót do poprzedniego obrazu lub produkt z innej gałęzi.
W naszym przykładzie F2 deklaruje obsługiwane formaty, a konfiguracja wybiera jeden z nich dla konkretnego kanału. Nie oznacza to swobodnego negocjowania każdego pola. Liczba kombinacji musi pozostać ograniczona i objęta macierzą testów. Deklaracja możliwości nie zastępuje uwierzytelnienia: obce urządzenie nie uzyskuje dostępu dlatego, że umie przesłać poprawne schema: 2.
Warto zachować w telemetrii osobno identyfikator wydania firmware, wersję kontraktu i aktywny tryb emisji. Gdy po aktualizacji rośnie liczba odrzuceń, można wtedy ustalić, czy problem dotyczy parsera, konfiguracji, czy określonego wydania. Sam napis „klient nowy” nie pozwala odtworzyć kombinacji, która rzeczywiście wystąpiła.
Zmiana liczby może zmienić decyzję bez zmiany typu
Rozważmy jeszcze pozornie kosmetyczną zmianę: serwer zaczyna zaokrąglać temperaturę do jednego miejsca po przecinku zamiast przechowywać przesłaną wartość. Dla próbki 21,46 i reguły temperatura > 21,48 wynik przed zaokrągleniem jest fałszywy, a po zaokrągleniu do 21,5 prawdziwy. Komunikat nadal ma liczbę w stopniach Celsjusza. Zmienił się jednak wynik działania.
Dlatego kontrakt powinien rozdzielać wartość używaną do obliczeń od sposobu jej prezentacji. Podobnie należy potraktować włączenie korekcji kalibracyjnej, zmianę filtrowania lub zastąpienie próbki chwilowej średnią z minuty. Każda może być sensowną poprawą produktu, lecz nie jest neutralna dla odbiorcy zakładającego poprzednią wielkość mierzoną.
Testy graniczne wybieramy przy progach, na których klient podejmuje decyzję, a nie tylko przy wygodnych wartościach środkowych. W archiwum zachowujemy informację o zastosowanym przetwarzaniu, jeżeli bez niej dawnych i nowych wartości nie można uczciwie porównać. Wersjonowanie takiego znaczenia bywa ważniejsze niż kosmetyczna zmiana nazwy endpointu.
Migracja zaczyna się od czytania
Bezpieczna kolejność wynika z tego, kto pierwszy zobaczy nowy format. Najpierw przygotowujemy odbiorców i zapis do obu wersji, zachowując dotychczasową emisję. Potem włączamy v2 dla ograniczonej grupy nadajników. Dopiero po obserwacji współistnienia można rozważyć wycofanie starej ścieżki.
Pierwsza bramka odbioru obejmuje usługę, magazyn danych, eksport, aplikację oraz zadania działające w tle. Każdy element musi umieć przyjąć nowy rekord albo jawnie go odseparować. Sam nowy endpoint nie wystarczy, jeżeli nocny raport zakłada, że temperatura zawsze jest liczbą. Test powinien przeprowadzić przez całą ścieżkę zarówno poprawny wynik, jak i brak wyniku.
W drugim etapie wybieramy urządzenia pilotażowe i zapisujemy, jaka możliwość została włączona. Potwierdzenie obsługi v2 powinno dotyczyć właściwego kanału i aktualnej konfiguracji. Zapamiętana przed miesiącem deklaracja serwera może przestać odpowiadać rzeczywistości po cofnięciu wdrożenia. Błąd negocjacji ma prowadzić do jawnie zdefiniowanego trybu, nie do wysyłania danych w losowo wybranym formacie.
W okresie współistnienia monitorujemy nie tylko błędy parsowania, ale także odrzucenia semantyczne, liczbę legacy_unknown, nieobsługiwane stany oraz udział danych przeterminowanych. Warto oddzielić urządzenia aktywne od tych, które nie zgłosiły się od dawna. Brak błędów od urządzenia offline nie jest dowodem, że przeszło migrację.
Rollback dotyczy również zapisanych danych
Wyobraźmy sobie, że S2 działa przez tydzień i zapisuje v2. Następnie wykryto błąd niezwiązany z formatem, a zespół chce wrócić do poprzedniego obrazu usługi. S1 nie umie odczytać zapisanych rekordów. Wycofanie kodu może więc naprawić jeden problem i jednocześnie przerwać odczyt historii.
Dlatego plan odwrotu przygotowujemy przed emisją pierwszego v2. Wariantem może być poprzednie zachowanie aplikacji uruchomione na wersji nadal czytającej oba schematy. Innym rozwiązaniem jest odseparowany adapter, ale musi zachowywać ograniczenia jakości. Konwersja missing na zero tylko po to, by stary raport się nie wywrócił, nie jest poprawną migracją danych.
Próba rollbacku powinna obejmować dane zapisane przed zmianą, w czasie współistnienia i po jej włączeniu. Sprawdzamy także ponowne przetwarzanie komunikatu z kolejki oraz urządzenie wracające po długiej przerwie. Jeśli magazyn został zmieniony nieodwracalnie, nie nazywamy powrotu do starego obrazu dostępną ścieżką awaryjną. Dokumentujemy realną procedurę przywrócenia działania.
Oddzielmy również przechowywany pomiar od bieżącego stanu. Historyczny rekord może zachować informację, że w chwili powstania urządzenie uznało próbkę za poprawną. Po roku nie powinien jednak przejść testu aktualności dla działania wykonywanego teraz. Odczyt archiwum nie zmienia czasu pomiaru na czas odczytu. To drobna pomyłka implementacyjna, która potrafi sprawić, że wykres i automatyzacja mówią o różnych chwilach.
Kiedy można wycofać v1
Termin w kalendarzu nie wystarczy. Potrzebujemy informacji o aktywnej flocie, maksymalnym przewidywanym czasie offline, integracjach klienta i zapisanych danych. Jeżeli urządzenie może wrócić po pół roku, miesiąc bez ruchu v1 nie dowodzi braku zależności. Jednostki nieobjęte wsparciem również wymagają świadomej decyzji, co zobaczą po ponownym połączeniu.
Przy wycofaniu starego kontraktu nie usuwamy automatycznie czytnika historii. Czas życia wejścia sieciowego i czas życia danych archiwalnych są różne. Można zakończyć przyjmowanie nowych v1, a nadal obsługiwać odczyt dawnych rekordów przez stabilny adapter. Jego odpowiedzialność jest wtedy węższa i łatwiejsza do przetestowania niż utrzymywanie całej starej usługi.
Zespół powinien także sprawdzić eksporty poza własnym systemem. Klient może mieć skrypt, który pobiera tylko liczbę, oraz arkusz interpretujący brak komórki jako zero. Bez rozpoznania tych odbiorców deklaracja pełnej zgodności będzie zbyt szeroka. Dokumentacja migracji powinna pokazać konkretne zmiany odpowiedzi i znaczenia stanów, a nie jedynie ogłosić nowy numer wersji.
Karta przeglądu jednej zmiany kontraktu
Przed akceptacją zmiany warto wypełnić krótką tabelę. Najważniejsza jest ostatnia kolumna: obserwowalny wynik, na podstawie którego można odrzucić błędną implementację.
W razie potrzeby przewiń tabelę poziomo.
| Obszar | Pytanie | Dowód odbioru |
|---|---|---|
| Znaczenie | Co oznaczała stara wartość i co oznacza nowa? | Przykład decyzji odbiorcy przed zmianą i po niej |
| Brak danych | Czy brak różni się od zera i starego wyniku? | Przypadki 0, null, stale i braku wiadomości |
| Rozszerzenia | Co zrobi odbiorca z nieznanym polem lub stanem? | Jawne odrzucenie albo udokumentowane ograniczenie |
| Współistnienie | Które kombinacje wersji są obsługiwane? | Macierz z testem każdego dozwolonego pola |
| Historia | Czy stare i nowe rekordy można odczytać ponownie? | Odtworzenie archiwum i kolejki |
| Cofnięcie | Jaka wersja odczyta dane zapisane po zmianie? | Próba rollbacku z rekordami v2 |
| Wycofanie | Co stanie się ze spóźnionym urządzeniem? | Scenariusz powrotu po okresie offline |
Dobry kontrakt nie obiecuje, że każda przyszła funkcja zadziała na każdym starym urządzeniu. Określa, gdzie zachowanie pozostaje takie samo, gdzie wiedza jest ograniczona i gdzie potrzebna jest migracja. W naszym przykładzie najważniejsza decyzja brzmi: brak aktualnej temperatury ma pozostać brakiem, nawet jeśli utrzymanie starego klienta byłoby łatwiejsze po podstawieniu znajomej liczby.
Źródła
- Google AIP-180: Backwards compatibility, rozróżnienie zgodności źródłowej, komunikacyjnej i semantycznej; wskazówki nie są kompletną listą wszystkich przypadków.
- Protocol Buffers: Language Guide, proto3, Updating a Message Type, zasady zmian formatu binarnego, numery pól i wartości domyślne.
- Protocol Buffers: Application Note, Field Presence, jawna obecność pól skalarnych i ryzyko utraty tej informacji podczas ponownej serializacji.
- Protocol Buffers: ProtoJSON Format, nieznane pola i odrębne reguły ewolucji reprezentacji JSON.
- RFC 8259: The JavaScript Object Notation Data Interchange Format, grudzień 2017, w szczególności sekcja 6 o liczbach.
Dokumentację sprawdzono 21 października 2026 r. Przykład Protobuf dotyczy składni proto3, nie domyślnych reguł Editions; nie jest wynikiem testu kompilatora ani konkretnego runtime. Sprawdzone scenariusze dotyczą opisanego demonstratora JSON i jego decyzji semantycznych.