CodeGym /Kursy /Java Server /JSON jako string nie wystarczy: Jackson 3

JSON jako string nie wystarczy: Jackson 3

Java Server
Poziom 16, Lekcja 0
Dostępny

1. Teraz: JSON to nadal String

Jeśli spojrzeć na aktualną wersję ReadLater Starter po refaktoringu warstwy transportowej, obraz jest dość uczciwy: żądania wychodzą, odpowiedzi wracają, status można sprawdzić, JSON można wypisać. Ale z punktu widzenia aplikacji dane wciąż wyglądają jak „worek znaków”. To normalne na starcie – dokładnie tak zaczyna większość ludzi. Problem zaczyna się wtedy, gdy chcesz nie tylko popatrzeć na JSON, ale go używać.

W rzeczywistości warstwa transportowa przynosi nam mniej więcej coś takiego (w uproszczeniu):


import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

// HTTP-klient – niskopoziomowy transport: na razie nie wie nic o „książkach”, „autorach” itd.
HttpClient client = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder()
        // Ważne: tutaj operujemy tylko URL-em i metodą HTTP – to jeszcze nie jest „model aplikacji”
        .uri(URI.create("https://catalog.example/api/search?q=clean%20code"))
        .GET()
        .build();

// Na razie wynik to tylko status + string (raw JSON)
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

// Sprawdzamy wynik transportowy
System.out.println(response.statusCode()); // 200
// I drukujemy ciało takie, jakie jest (to jeszcze nie „dane aplikacji”, tylko opakowanie)
System.out.println(response.body());       // {"docs":[...],"numFound":123}

Z punktu widzenia „umiemy chodzić po HTTP” – to zwycięstwo. Ale z punktu widzenia „budujemy aplikację” od razu pojawia się przyziemne pytanie: jak teraz dostać listę książek, ich tytuły i autorów tak, żeby kod był czytelny i nie psuł się przy pierwszym kichnięciu dostawcy?

Można oczywiście dalej żyć w trybie „no widziałem gdzieś, że tam jest pole docs, zaraz wyciągnę je substring-iem”. I tu zaczyna się to, co programiści nazywają „historią sukcesu, ale w gatunku horror”.

2. Surowy string JSON: „piasek w trybach”

Praca z JSON-em jak ze stringiem to jak próba gotowania zupy bez noża, używając zamiast niego karty bankowej. Technicznie się da, ale gdzieś po trzeciej marchewce zaczniesz myśleć o sensie życia, a nie o przepisie. Metody stringowe (contains, indexOf, substring) nie rozumieją struktury JSON-a: widzą tylko znaki, a nie pola, tablice i zagnieżdżenie.

Najbardziej niewinny przykład zwykle wygląda tak:


 // Raw JSON jako string (w ogóle nie rozumiemy struktury, widzimy tylko znaki)
 String rawJson = """
     {"title":"Clean Code","author":"Robert C. Martin"}
     """;

 // Sprawdzenie na poziomie „w stringu jest jakiś podciąg”
 boolean hasTitle = rawJson.contains("\"title\"");
 System.out.println(hasTitle); // true

I niby nawet nie wstyd. Ale to sprawdzenie poziomu „w tekście jest słowo title”. Ono nie mówi nam, jaka jest wartość title, nie gwarantuje, że to dokładnie to pole, którego potrzebujemy, i w ogóle łatwo da się oszukać czymkolwiek.

Następny krok nowicjusza – wyciągnąć wartość rękami:


String rawJson = """
    {"title":"Clean Code","author":"Robert C. Martin"}
    """;

// Szukamy początku wartości pola title (kruche: zależy od formatu, spacji, kolejności pól)
int start = rawJson.indexOf("\"title\":\"") + "\"title\":\"".length();
// Szukamy najbliższego cudzysłowu jako „końca wartości” (psuje się przy escape'ach i cudzysłowach w środku)
int end = rawJson.indexOf("\"", start);

// Wycinamy kawałek stringa: „parsowanie”, które tak naprawdę nie parsuje JSON-a
String title = rawJson.substring(start, end);
System.out.println(title); // Clean Code

Dopóki JSON jest idealnie równy, bez escape'ów, bez spacji, bez zagnieżdżenia i bez powtarzających się kluczy – to nawet będzie działać. Ale rzeczywisty zewnętrzny JSON niemal natychmiast łamie to „podejście” z kupy powodów.

Jeśli dostawca doda spacje albo zmieni kolejność pól, logika stringowa może nagle przestać znajdować potrzebny kawałek. Jeśli w wartości pojawią się cudzysłowy, znaki nowej linii lub jakieś znaki specjalne, twój indexOf zacznie „ciąć” w złym miejscu. Jeśli title okaże się wewnątrz tablicy obiektów, a nie w korzeniu – zaczniesz pisać parser tablic. Spoiler: przypadkowo napiszesz swojego „Jacksona”, tylko gorszego, wolniejszego i z bugami. To normalna droga – ale lepiej przejść ją w teorii, a nie na produkcji i nie w projekcie kursowym.

I najprzykrzejsze: parsowanie stringowe sprawia, że błędy są nieoczywiste. Często nie wywala się ładnie, tylko „trochę nieprawidłowo” wyciąga dane, a ty potem długo zastanawiasz się, dlaczego u książki autor nagle wynosi "Martin","status":"PLANNED"... (tak, tak bywa, jeśli przypadkowo trafiłeś substring-iem w niewłaściwe miejsce).

3. Warstwa mapowania: JSON → DTO

Gdy aplikacja rośnie, bardzo przydaje się w niej mieć jasne granice odpowiedzialności. Warstwa transportowa już stała się taką granicą: chodzi po HTTP i zwraca status + raw body. Teraz potrzebujemy następnego „przejściówki”: warstwy, która zamienia string JSON w obiekt Javy o zrozumiałej formie. To właśnie jest warstwa mapowania JSON – osobny krok między response.body() a twoim kodem aplikacyjnym.

Jeśli przedstawić ideę schematycznie, wyjdzie mniej więcej tak:

flowchart LR
    %% Idea: „string JSON” żyje na granicy, a wewnątrz aplikacji – typy i pola
    A["HTTP response.body() String"] --> B["JSON mapping layer Jackson"]
    B --> C["Typed DTO Java record"]
    C --> D["Dalej kod pracuje z polami i typami, a nie ze stringiem"]

Z praktycznego punktu widzenia warstwa mapowania daje trzy bardzo przyziemne korzyści.

Pierwsza korzyść – zaczynasz pracować z danymi przez pola i typy, a nie przez „magiczne” kawałki stringa. book.title() czyta się jak ludzki tekst. substring(17, 42) czyta się jak „zmęczyłem się, szefie”.

Druga korzyść – błędy stają się lokalne. Jeśli JSON nie zgadza się z oczekiwaną formą, problem ujawnia się dokładnie na granicy JSON → DTO, a nie 15 linijek później, gdy gdzieś przypadkowo dostałeś null i wywaliłeś się w innym miejscu.

Trzecia korzyść – wzmacniasz dyscyplinę: raw JSON pozostaje na granicy transport/mapping, a do środka aplikacji nie przecieka. To bardzo ważny nawyk przed Springiem: później framework wiele zautomatyzuje, ale sama idea granic przez to nie znika.

4. Jackson 3: tłumacz JSON ↔ Java

Żeby nie wymyślać koła z makaronu i plasteliny, bierzemy bibliotekę, która profesjonalnie zajmuje się tłumaczeniem między JSON-em a obiektami Javy. W naszym kursie to Jackson 3 – i używamy go jako jednolitej bazy. Centralna klasa do codziennej pracy to ObjectMapper, a w Jackson 3 znajduje się w pakiecie tools.jackson.databind.

Najbardziej podstawowa magia (tak naprawdę nie magia, tylko normalne narzędzie) wygląda tak: ogłaszamy DTO, a potem prosimy Jacksona, żeby wczytał JSON do tego DTO.


import tools.jackson.databind.ObjectMapper;

public class JacksonMiniDemo {

    // DTO (kontrakt): w takiej formie chcemy widzieć dane wewnątrz aplikacji
    record BookDto(String title, String author) {}

    public static void main(String[] args) throws Exception {
        // Raw JSON przyszedł jako string (np. z response.body())
        String rawJson = """
            {"title":"Clean Code","author":"Robert C. Martin"}
            """;

        // ObjectMapper – nasz „tłumacz” między JSON-em a typami Javy
        ObjectMapper mapper = new ObjectMapper();

        // Deserializacja: JSON -> typowane DTO
        BookDto book = mapper.readValue(rawJson, BookDto.class);

        // Teraz pracujemy z polami, a nie z podciągami
        System.out.println(book.title()); // Clean Code
    }
}

Zwróć uwagę na ważny moment psychologiczny: w kodzie nie ma już „szukania podciągu \"title\":\"”. Po prostu mówimy: „oto kontrakt, oto DTO, przetłumacz”.

Operacja odwrotna też istnieje: ten sam ObjectMapper umie złożyć JSON z obiektu Javy. Ale tutaj ważniejsze jest dla nas właśnie czytanie. Gdy tylko response.body() szybko zamienia się w DTO, aplikacja przestaje żyć w świecie substring i zaczyna pracować z typami.

Na razie wystarczy uchwycić samą zasadę: ObjectMapper to granica między stringowym JSON-em a typowanymi danymi aplikacji. Osobny nawyk inżynierski – trzymać takie mapowanie zebrane w jednym miejscu i nie rozsmarowywać go po kodzie.

5. Baza: Jackson 3 bez Jackson 2

W internecie znajdziesz tony przykładów „jak parsować JSON w Javie”. Problem w tym, że znaczna część tych przykładów jest napisana pod Jackson 2, a Jackson 3 to nie zwykła „następna wersja” – tam naprawdę zmieniła się ważna część „punktów wejścia”: koordynaty Maven i pakiety Javy. W Jackson 3 główne pakiety przeprowadziły się do przestrzeni nazw tools.jackson.*, żeby Jackson 2 i Jackson 3 mogły współistnieć, nie konfliktując na classpath.

Oto krótka tabelka, która ratuje przed 80% bólu nowicjuszy:

Co widzisz w przykładzie To prawdopodobnie Co robić w naszym kursie
implementation("com.fasterxml.jackson.core:jackson-databind:...") Jackson 2 Nie kopiować „na ślepo”
implementation("tools.jackson.core:jackson-databind:3.0.4") Jackson 3 To nasza baza
import com.fasterxml.jackson.databind.ObjectMapper; Jackson 2 U nas się tak nie skompiluje
import tools.jackson.databind.ObjectMapper; Jackson 3 Tak właśnie trzeba
import com.fasterxml.jackson.annotation.JsonProperty; Adnotacje Jacksona To normalne nawet w Jackson 3 (adnotacje zostały w starym groupId)

I tu kryje się podstępna pułapka. Gdy student widzi błąd importu, często wykonuje „logiczny” ruch: dodaje jeszcze jedną zależność, żeby „naprawić import”. Na przykład w projekcie jest już Jackson 3, ale dorzuciłeś Jackson 2 „bo artykuł tak powiedział”. W rezultacie wychodzą dwie różne biblioteki, które robią mniej więcej to samo, ale żyją w różnych pakietach. Tak, właśnie dlatego Jackson 3 przeprowadził się do tools.jackson.*: żeby współistnienie było możliwe.

Tylko współistnienie to możliwość dla dużych projektów, które migrują stopniowo. Dla projektu edukacyjnego zwykle zamienia się to w chaos: zaczynasz się mylić, którego ObjectMapper używasz, jakie wyjątki łapać, jakie zależności są naprawdę potrzebne. Dlatego zasada kursu jest prosta i lekko nudna (czyli pożyteczna): trzymamy jedną bazę Jackson 3 i nie robimy w projekcie „zoo wersji”.

Jeszcze jeden szczegół, który wygląda jak drobiazg, ale stale psuje życie: w Jackson 3 jackson-databind rzeczywiście zależy od jackson-annotations, a w POM jest wprost wyjaśnienie, że adnotacje zostają w groupId Jackson 2 (com.fasterxml.jackson.core). Czyli kombinacja tools.jackson.databind.ObjectMapper + com.fasterxml.jackson.annotation.JsonProperty – to nie błąd, tylko normalna część koncepcji Jackson 3.

6. ReadLater Starter: od response.body() do DTO

Żeby nie odbierać Jacksona jako „jeszcze jednego tematu w próżni”, podpnijmy wszystko do naszego projektu. Mamy komendę typu catalog search <query>, która wykonuje wywołanie HTTP do zewnętrznego katalogu. Warstwa transportowa zwraca nam status i string JSON. Następny krok to nie „ładniej wypisać string”, tylko zamienić go w DTO, bo właśnie DTO będzie dalej normalizowane i pokazywane użytkownikowi.

W uproszczeniu tok myśli w kodzie powinien stać się taki:


import tools.jackson.databind.ObjectMapper;
import java.net.http.HttpResponse;

public class CatalogFlowSketch {

    // DTO udanej odpowiedzi dostawcy (w rzeczywistości pól będzie więcej)
    record ProviderSearchResponseDto(int numFound) {}

    public ProviderSearchResponseDto parse(HttpResponse<String> response) throws Exception {
        // Najpierw – sprawdzenie transportowe: status, błędy, retry itd.
        if (response.statusCode() != 200) {
            // W tym miejscu wprost zaznaczamy, że problem nie tkwi w mapowaniu JSON, tylko w odpowiedzi dostawcy
            throw new IllegalStateException("Provider returned status " + response.statusCode());
        }

        // Potem – mapowanie: zamieniamy string JSON na typ
        ObjectMapper mapper = new ObjectMapper();
        return mapper.readValue(response.body(), ProviderSearchResponseDto.class);
    }
}

Tu ważna jest sama granica: po sprawdzeniu statusu JSON powinien szybko zamienić się w DTO, a nie pojechać stringiem dalej po projekcie. Osobny nawyk inżynierski – nie tworzyć ObjectMapper od nowa w każdym miejscu i nie rozsmarowywać logiki mapowania po kodzie. Ale nawet w tak krótkim szkicu widać już to, co najważniejsze: sukces transportowy i mapowanie JSON-a to dwa różne kroki i nie warto ich mylić.

7. Typowe błędy

Na początku pracy z Jacksonem niemal wszystkie błędy wynikają nie z tego, że „Jackson jest skomplikowany”, tylko z tego, że mózg z rozpędu ciągnie cię z powrotem do świata stringów i „szybkich rozwiązań”. To normalne: uczysz się. Najważniejsze – w porę złapać nawyk za rękę i delikatnie, ale stanowczo wrócić go do cywilizacji.

Błąd nr 1: dalej parsować JSON metodami stringowymi „bo tak szybciej”.
Zwykle zaczyna się od contains, potem pojawia się substring, potem „mały regex”, a potem nagle piszesz obsługę escape'owania cudzysłowów i rozumiesz, że właśnie zacząłeś projektować bibliotekę poziomu Jackson, tylko bez wypłaty. Właściwe przejście na tym dnie – traktować string jako opakowanie transportowe, które trzeba rozpakować do DTO i wyrzucić.

Błąd nr 2: próbować zmapować body przed sprawdzeniem statusCode.
ObjectMapper nie ma obowiązku „zgadywać”, co dostałeś. Jeśli dostawca zwrócił 404 lub 500 z innym JSON-em, a ty próbujesz wczytać to jako udane DTO, dostaniesz błąd deserializacji w miejscu, gdzie tak naprawdę problem leży na poziomie transportu. Dyscyplina jest prosta: najpierw rozgałęzienie po statusie, potem JSON → DTO.

Błąd nr 3: kopiować przykłady z Jackson 2 i „naprawiać” je dodawaniem nowych zależności.
Jeśli w artykule ObjectMapper jest importowany z com.fasterxml.jackson.databind, a u ciebie w projekcie jest Jackson 3, nie trzeba ratować sytuacji dodaniem Jackson 2. U nas baza to Jackson 3, a ObjectMapper powinien być z tools.jackson.databind. Adnotacje przy tym mogą zostać com.fasterxml.jackson.annotation.* – to jest oczekiwane.

Błąd nr 4: przeciągać raw JSON przez kilka warstw „na wszelki wypadek”.
Czasem wydaje się, że string warto zachować „na potem”: a nuż się przyda. W praktyce prowadzi to do tego, że połowa kodu zaczyna zależeć od formatu dostawcy, a wszelkie zmiany zewnętrznego kontraktu rozlatują się po całym projekcie. Znacznie spokojniej jest trzymać raw JSON tylko na granicy transport/mapping, a do środka oddawać DTO.

1
Zadanie
Java Server, poziom 16, lekcja 0
Niedostępne
Jackson 3 zamiast ręcznego parsowania JSON
Jackson 3 zamiast ręcznego parsowania JSON
1
Zadanie
Java Server, poziom 16, lekcja 0
Niedostępne
Osobna klasa mapująca dla pojedynczego łańcucha JSON
Osobna klasa mapująca dla pojedynczego łańcucha JSON
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION