CodeGym /Kursy /Spring REST & MVC /@JsonAlias i aliasy w wejściowym JSON

@JsonAlias i aliasy w wejściowym JSON

Spring REST & MVC
Poziom 12, Lekcja 1
Dostępny

1. Alias w żywych klientach i refactoringu

Wyobraź sobie, że twoje API ktoś już używa. Niekoniecznie tysiące klientów, wystarczy jeden frontend, który jest wdrażany raz w tygodniu, i jeden skrypt integracyjny, który „piszemy raz na zawsze" (a „na zawsze" trwa do pierwszej awarii w piątkowy wieczór). W takiej rzeczywistości zmiana nazwy pola JSON zamienia się w ból: serwer chce jednej nazwy, klient wciąż wysyła starą i łapiesz błędy z niczego.

W projekcie edukacyjnym łatwo to zasymulować: powiedzmy, na samym początku zrobiłeś pole assignee (kto jest przypisany do zadania). Potem zdecydowałeś, że w API lepiej brzmi assigneeName, bo to właśnie imię, a nie jakiś zagadkowy obiekt użytkownika. Serwer zmieniony, frontend jeszcze nie zdążył. I co robić?

Najbardziej „uczciwa" odpowiedź – powiedzieć klientowi: „aktualizujcie się" i zwrócić 400 Bad Request. Ale czasem potrzebujesz miękkiego przejścia: przyjmować i starą, i nową nazwę, a w odpowiedziach nadal oddawać jedną kanoniczną nazwę. @JsonAlias jest właśnie o tym: nie o „pozwólmy na wszystko", lecz o starannej kompatybilności na wejściu.

Kanoniczna nazwa i alias: role

Ważne, żeby nie pogubić się w terminach, bo stąd biorą się typowe błędy na poziomie „myślałem, że alias będzie też w odpowiedzi". Kanoniczna nazwa – to to, co uważasz za właściwą nazwę w kontrakcie. Alias – to „przydomek", który tymczasowo jesteś gotów przyjąć na wejściu.

Bardzo przydatne jest trzymanie tej różnicy w głowie jako „dwie strony jednego pola":

Pojęcie Gdzie używane Co daje Czego NIE daje
Kanoniczna nazwa pola W dokumentacji, odpowiedziach serwera, przykładach Stabilny zewnętrzny kontrakt Automatycznej obsługi starych nazw
Alias (alternatywna nazwa) Tylko przy odczycie wejściowego JSON Miękkie przyjmowanie starych/alternatywnych kluczy Pojawiania się tych nazw w odpowiedziach serwera

Jeśli chcesz prostej życiowej analogii: kanoniczna nazwa to imię w dowodzie osobistym, a alias to „Maciek, którego wszyscy wołają Maćku". W szkole to urocze, ale w umowie najmu mieszkania jednak piszemy dane z dowodu.

Z punktu widzenia naszego API oznacza to: klient może przysłać assignee, ale serwer wewnętrznie i tak wypełni pole assigneeName. Gdy serwer oddaje JSON z powrotem – mówi do świata kanonicznymi nazwami. Inaczej zamieniasz API w rozmówkę „dziś mówimy tak, jutro tak".

2. @JsonAlias w Jackson: tylko odczyt

@JsonAlias to adnotacja Jacksona, która dodaje alternatywne nazwy dla właściwości przy deserializacji. Czyli wpływa na moment, gdy JSON zamienia się w obiekt Java (nasz request DTO). Na serializację (obiekt → JSON) nie wpływa.

Brzmi prosto, ale jest kilka ważnych konsekwencji.

Po pierwsze, @JsonAlias to historia o request body. Działa tam, gdzie pracuje Jackson: @RequestBody i HttpMessageConverter. Na parametry query (@RequestParam) to się nie rozciąga, bo parametry query parsuje i konwertuje Spring MVC mechanizmem type conversion, a nie Jackson.

Po drugie, alias to „druga nazwa tego samego sensu". Jeśli zaczynasz podsuwać alias z innego pojęcia, robisz kontrakt nie elastycznym, lecz niejednoznacznym. Na przykład assignee i assigneeName można uznać za jeden sens (oba o imieniu wykonawcy), a owner lub reporter – to już inne pole, nawet jeśli „u nas w firmie tak kiedyś mówiono".

Po trzecie, warto pamiętać o niejednoznaczności: jeśli klient prześle oba klucze w jednym JSON (i assigneeName, i assignee), to to już żądanie na poziomie „sam nie rozumiem, co wysyłam". Jackson odczyta dokument sekwencyjnie i ostatecznie właściwość będzie miała jedną wartość, ale którą konkretnie – zależy od kolejności kluczy w JSON. To zbyt kruche, więc takie wejście lepiej uznać za błędne na poziomie dyscypliny klienta (a w dojrzałym API – jeszcze explicite walidować/zabraniać, ale to osobny temat).

3. Przykład: assignee jako alias do assigneeName

W naszym Task Tracker API najbardziej życiowy przykład – pole wykonawcy zadania. Powiedzmy, kanoniczna nazwa w kontrakcie jest już ustalona jako assigneeName, ale chcemy tymczasowo przyjmować starą assignee w wejściowym JSON.

Zacznijmy od request DTO do tworzenia zadania. Weźmy lokalny fragment request DTO tylko dla mechaniki alias: pól jest tu dokładnie tyle, ile potrzeba dla jednej idei. Pozostałe szczegóły żądania create nie są teraz ważne.

package com.example.tasktracker.api.dto.request;

import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonProperty;

public record TaskCreateRequest(
    String title,
    // Kanoniczna nazwa w kontrakcie (to, co uważamy za „właściwe" i pokazujemy na zewnątrz)
    @JsonProperty("assigneeName")
    // Alias: przyjmujemy starą nazwę pola tylko przy odczycie wejściowego JSON
    @JsonAlias("assignee")
    String assigneeName
) {
    // Tu pusto: record to DTO, przechowuje dane, a nie implementuje logikę biznesową
}

Zwróć uwagę na ideę: wewnątrz kodu Java też nazywamy komponent assigneeName, żeby nie mylić warstwy serwisowej. @JsonProperty("assigneeName") utrwala kanoniczną nazwę, a @JsonAlias("assignee") dodaje alternatywę do odczytu.

Teraz dwóch klientów może przysłać semantycznie to samo żądanie różnymi kluczami i serwer odczyta obydwa.

Przykład żądania ze „starą" nazwą pola:

# Klient „starej wersji": wciąż wysyła assignee
POST http://localhost:8080/api/v1/tasks
Content-Type: application/json

{
  "title": "Write API docs",
  "assignee": "Alice"
}

A takie żądanie używa już kanonicznej nazwy:

# Klient „nowej wersji": wysyła kanoniczne pole assigneeName
POST http://localhost:8080/api/v1/tasks
Content-Type: application/json

{
  "title": "Write API docs",
  "assigneeName": "Alice"
}

W obu przypadkach w kontrolerze dostaniesz TaskCreateRequest, gdzie assigneeName() zwróci "Alice". Alias „scalił się" już na etapie deserializacji i dalej twój kod żyje tak, jakby klient zawsze był zdyscyplinowany.

Żeby podkreślić, że alias nie „rozlewa się" po aplikacji, można pokazać kawałek serwisu lub mappera: tam w ogóle nie powinno być warunków w stylu „jeśli przysłali assignee – bierz to, jeśli assigneeName – bierz tamto". Wszystko już sprowadzone do jednego pola.

package com.example.tasktracker.api.mapper;

import com.example.tasktracker.api.dto.request.TaskCreateRequest;

public final class TaskMapper {

    private TaskMapper() {
        // Klasa narzędziowa: bronimy się przed przypadkowym tworzeniem instancji
    }

    public static String extractAssigneeName(TaskCreateRequest request) {
        // Ważne: tu nie ma rozgałęzień po starej/nowej nazwie.
        // Alias został już obsłużony przez Jacksona na etapie deserializacji.
        return request.assigneeName();
    }
}

Kod jest celowo prosty, ale o to właśnie chodzi: alias nie komplikuje warstwy serwisowej.

4. Przykład: deadline jako alias do dueDate

Druga częsta historia – daty. Różne zespoły i różne systemy nazywają termin na różne sposoby: ktoś mówi „deadline", ktoś „due date", ktoś „planned finish". W API chcemy jednego zrozumiałego terminu, ale na wejściu czasem trzeba się „zabezpieczyć".

Powiedzmy, w naszym kontrakcie kanoniczna nazwa to dueDate, a alias – deadline. Przy tym używamy normalnego typu czasu (np. LocalDate), który omawialiśmy w poprzedniej lekcji.

package com.example.tasktracker.api.dto.request;

import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.LocalDate;

public record TaskSchedulingRequest(
    // Kanoniczna nazwa w kontrakcie
    @JsonProperty("dueDate")
    // Przyjmujemy alternatywny klucz od starych klientów/zewnętrznych integracji
    @JsonAlias("deadline")
    LocalDate dueDate
) {
    // DTO do planowania: jedno pole, jeden sens
}

Tu celowo wydzieliłem przykład do osobnego małego DTO, żeby nie rozbudowywać TaskCreateRequest w przykładzie. W prawdziwym projekcie dodałbyś to pole tam, gdzie naprawdę żyje (zwykle w create/put-request).

Teraz serwer przyjmie i takie wejście:

{ "deadline": "2026-03-30" }

i takie:

{ "dueDate": "2026-03-30" }

Ale gdy będziesz oddawać zadanie na zewnątrz w response DTO, nadal będziesz używać jednego pola dueDate, bo odpowiedź to „co obiecujemy", a żądanie to „co jesteśmy gotowi uprzejmie zrozumieć".

5. Ile aliasów to normalnie

Kiedy programista po raz pierwszy dowiaduje się o @JsonAlias, istnieje ryzyko „poprawienia życia wszystkim" przez dodanie pół słownika. W tym momencie API zaczyna przypominać człowieka, który jednakowo żwawo reaguje na „Aleksander", „Olek", „Hej ty" i „Słuchaj, stary". Technicznie słyszy, ale wygląda to podejrzanie.

Alias jest użyteczny, ale dodaje koszty utrzymania. Im więcej alternatywnych nazw przyjmujesz, tym więcej wariantów musisz trzymać w głowie, testować (choćby ręcznie żądaniami .http) i tłumaczyć kolegom. Do tego zwiększasz szansę, że klient przypadkowo prześle „prawie tę nazwę" i nie zauważysz błędu od razu.

Dobra reguła dla edukacyjnego (i często realnego) API: alias ma sens, gdy jest konkretny powód. Na przykład miałeś stary kontrakt i migrujesz na nowy. Albo jesteś zobowiązany przyjąć payload z zewnętrznego systemu, gdzie pole nazywa się inaczej, ale sens jest ten sam. We wszystkich pozostałych przypadkach alias to po prostu sposób na ukrycie braku dyscypliny.

Żeby łatwiej było „przyłapać siebie za rękę", możesz trzymać taką małą tabelę-kryterium:

Sytuacja Używać @JsonAlias? Dlaczego
Przemianowałeś pole w API, ale starzy klienci wciąż żyją Tak To miękki pomost migracji na wejściu
Chcesz przyjmować i snake_case, i camelCase „na wszelki wypadek" Raczej nie To rozmywa kontrakt i rodzi chaos
Dwa różne pojęcia są „prawie podobne" i chcesz je skleić jednym polem Nie Wyjdzie niejednoznaczność zamiast kompatybilności
Masz request DTO i response DTO jako tę samą klasę i próbujesz „naprawić adnotacjami" Nie Trzeba rozdzielić modele, a nie leczyć objaw

Zauważ: tabelka nie o „zakazane przez prawo", lecz o dyscyplinie. Jackson uczciwie zrobi to, co poprosisz. Pytanie tylko, czy twoje API będzie na tym lepsze.

6. Gdzie umieszczać @JsonAlias

W naszym projekcie Task Tracker API mamy dobry nawyk architektoniczny: request DTO leżą w api.dto.request, response DTO – w api.dto.response, a wewnętrzny model żyje osobno. Jest to wygodne nie tylko dla czytelności, ale też dla takich niuansów.

@JsonAlias jest prawie zawsze stosowny właśnie w request DTO, bo rozwiązuje problem „jak odczytać wejście". Response DTO zazwyczaj nie powinno zawierać aliasów z jednego prostego powodu: odpowiedź to twoja publiczna witryna. Jeśli zaczniesz trzymać aliasy w modelach odpowiedzi, sam sobie skomplikujesz życie pytaniem „którą z nazw oddajemy dziś?".

Jest jeszcze jedna praktyczna korzyść: kiedy alias żyje w request DTO, twoja warstwa serwisowa pozostaje maksymalnie „czysta". Serwis nie powinien wiedzieć, że kiedyś ktoś nazywał assigneeName słowem assignee. Serwisowi potrzebna jest informacja znaczeniowa, a nie historia ewolucji twojego JSON.

I to, nawiasem mówiąc, dobry test „zdrowia" decyzji: jeśli po dodaniu aliasu zachciało ci się zmieniać mapowanie, serwisy, model domenowy lub logikę kontrolera – najprawdopodobniej używasz aliasu nie do tego. W normalnym scenariuszu @JsonAlias to mały detal na DTO, który w ogóle nie powinien „wyciekać" dalej.

7. Typowe błędy przy używaniu @JsonAlias

Błąd nr 1: oczekiwanie, że alias zacznie wpływać na odpowiedź JSON.
To bardzo popularne oczekiwanie: „dodałem @JsonAlias("assignee"), więc serwer będzie oddawał assignee". Nie, nie będzie. @JsonAlias – tylko o odczycie wejściowego JSON. Jeśli chcesz zarządzać nazwą w odpowiedzi, użyj kanonicznej nazwy (przez @JsonProperty lub właściową nazwę pola). I pamiętaj: odpowiedź to kontrakt, a alias to „rozumiemy cię, nawet jeśli powiedziałeś po staremu".

Błąd nr 2: dodawanie aliasu dla pól różnych znaczeniowo.
Czasem wydaje się, że „owner", „assignee", „responsible" to prawie to samo. Ale w kontrakcie to różne role, i jeśli skleisz je jednym polem przez alias, przestaniesz rozróżniać sens. Klienci będą przysyłać różne klucze, mając na myśli różne rzeczy, a serwer po cichu złoży to do jednej właściwości. Wyjdzie nie elastyczność, lecz cicha błąd logiczny.

Błąd nr 3: rozdymanie aliasu w „słownik synonimów".
Jeśli pole ma pięć aliasów ("assignee", "assignee_name", "executor", "worker", "dudeWhoDoesIt"), nie robisz API wygodniejszym – robisz je nieprzewidywalnym. Do tego każdy nowy klient zaczyna myśleć: „a która nazwa jest prawdziwa?". Prawdziwa powinna być jedna, a alias – co najwyżej mały pomost, i to z powodu.

Błąd nr 4: używanie aliasu jako zamiennika normalnego rozdzielenia request/response DTO.
Jeśli bierzesz jeden DTO na wejście i wyjście i zaczynasz doklejać do niego @JsonAlias, @JsonIgnore, jeszcze kilka adnotacji – to prawie zawsze objaw. W takim modelu mieszasz w jednej klasie dwa różne obowiązki: przyjmowanie wejścia i bycie publiczną odpowiedzią. Właściwiej jest rozdzielić DTO według ról, a adnotacje zostawić jako precyzyjne narzędzie, a nie „szpachlę do architektury".

Błąd nr 5: uznawanie, że alias rozwiązuje problem „zbędnych pól" w wejściowym JSON.
Alias pomaga, gdy zbędne pole jest w rzeczywistości nie zbędne, tylko inaczej nazwane. Ale jeśli klient przysłał w ogóle nieznany klucz, @JsonAlias nie pomoże. Pytanie „co zrobić z nieznanymi polami" rozwiązują inne mechanizmy i inna polityka, a alias – to nie „tryb tolerancji", lecz „tryb synonimów".

1
Zadanie
Spring REST & MVC, poziom 12, lekcja 1
Niedostępne
Lozne przyjmowanie pola `host` jako aliasu dla `hostName`
Lozne przyjmowanie pola `host` jako aliasu dla `hostName`
1
Zadanie
Spring REST & MVC, poziom 12, lekcja 1
Niedostępne
Pole terminu `dueDate` z alternatywną nazwą wejściową `deadline`
Pole terminu `dueDate` z alternatywną nazwą wejściową `deadline`
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION