Gdy buduję API w JavaScript, JSON.stringify() traktuję jako granicę między obiektem w pamięci a tekstem, który można wysłać przez HTTP, zapisać w logu albo umieścić w odpowiedzi serwera. Temat js json stringify sprowadza się więc do jednego: jak bezpiecznie zamienić dane na JSON i nie zgubić po drodze ważnych pól, dat czy błędów. Pokażę tu, jak działa serializacja, co funkcja pomija, jak korzystać z replacer i space, oraz kiedy w backendzie łatwo o błąd, który wychodzi dopiero na produkcji.
Najważniejsze rzeczy o serializacji do JSON
-
JSON.stringify()zamienia wartość JavaScript na tekst JSON, więc bez niej nie wyślesz obiektu jako surowego body w żądaniufetch(). - Funkcja pomija część danych:
undefined, funkcje, symbole i pola nieenumerowalne. -
BigIntoraz struktury cykliczne kończą się błędem, jeśli nie przygotujesz własnej serializacji. -
replacerpozwala filtrować pola, aspacepoprawia czytelność wyniku w logach i debugowaniu. - W backendzie najbezpieczniej serializować kontrolowany DTO, a nie cały obiekt domenowy.
Czym jest JSON.stringify() i gdzie robi różnicę
JSON.stringify() bierze wartość JavaScript i zamienia ją na napis w formacie JSON. To ważne rozróżnienie, bo obiekt i JSON to nie to samo: obiekt żyje w pamięci aplikacji, a JSON jest tekstem, który da się przenieść między klientem, API, kolejką albo bazą logów.
W backendzie najczęściej używam tej funkcji przy budowie odpowiedzi API, przy wysyłaniu danych z frontendu oraz przy utrwalaniu prostych payloadów w cache lub logach. Jeśli w danym momencie potrzebuję „opakować” dane tak, żeby drugi system mógł je bezpiecznie odczytać, serializacja robi dokładnie tę robotę.
Tu łatwo też pomylić kierunki. JSON.stringify() tworzy tekst, a JSON.parse() albo Response.json() robią krok odwrotny. W przeglądarce Response.json() czyta body odpowiedzi i oddaje obiekt JavaScript, więc po drugiej stronie nie dostajesz JSON stringa, tylko już sparsowane dane. To drobna różnica składniowa, ale w praktyce decyduje o tym, czy kod zwraca stringi, czy prawdziwe obiekty.
Żeby zobaczyć, gdzie pojawiają się pułapki, warto przejść od definicji do realnego procesu serializacji.

Jak przebiega serializacja krok po kroku
Najprościej myśleć o tym jak o przepakowaniu danych z formy „obiekt” do formy „tekst”. Najpierw JSON.stringify() czyta strukturę wejściową, potem zamienia ją na poprawny JSON, a na końcu zwraca jeden string gotowy do wysłania lub zapisania.
Praktyczny przepływ w API wygląda zwykle tak:
- Po stronie klienta przygotowuję zwykły obiekt JavaScript.
- Przed wysłaniem w
fetch()zamieniam go na string przezJSON.stringify(). - Serwer odczytuje body, parsuje JSON i odzyskuje obiekt.
- W odpowiedzi często dzieje się odwrotny ruch: aplikacja serwerowa serializuje dane do JSON i odsyła je do klienta.
Warto zapamiętać, że sam JSON nie jest obiektem. To tylko tekstowy zapis struktury. Dlatego w praktyce bardzo często kończy się to parą serializacji na wyjściu i parsowania po drugiej stronie.
Dobry skrót myślowy jest prosty: jeśli coś ma opuścić proces jako body HTTP, log albo wiadomość w kolejce, najpierw pytam siebie, czy to już jest tekst. Jeśli nie, to właśnie tutaj wchodzi serializacja. Gdy dane są już „po drodze”, zaczynają się pytania o to, co dokładnie przejdzie, a co zniknie.
Co funkcja pomija, zmienia albo zamienia na null
Tu zaczynają się rzeczy, które najczęściej zaskakują nawet osoby piszące na co dzień w JavaScript. JSON.stringify() nie serializuje wszystkiego „jak leci”, tylko stosuje konkretne reguły.
| Wartość wejściowa | Co trafia do JSON | Znaczenie praktyczne |
|---|---|---|
undefined w obiekcie |
Pole znika | Nie zakładaj, że brak pola oznacza to samo co null. |
undefined w tablicy |
null |
Tablica zachowuje indeksy, ale dane są „wyczyszczone”. |
| Funkcja | Znika w obiekcie, w tablicy staje się null
|
Metody i callbacki nie są częścią JSON. |
Symbol |
Pomijany | Klucze symboliczne nie przechodzą do JSON. |
Gołe undefined, funkcja lub symbol |
undefined |
Nie dostajesz JSON stringa w ogóle, więc sprawdzaj wynik przed wysłaniem. |
BigInt |
Błąd TypeError
|
Musisz sam zdecydować, jak go zamienić, np. na string. |
| Obiekt z cyklem | Błąd TypeError
|
Najpierw usuń lub zamień odwołania cykliczne. |
Date |
String ISO | To zwykle bezpieczne dla API, bo format jest przewidywalny. |
Map / Set
|
Zwykle {}
|
Jeśli chcesz je wysłać, potrzebujesz własnej transformacji. |
MDN opisuje też ważny detal: serializacja obejmuje wyłącznie własne, enumerowalne właściwości. Dlatego pola nieenumerowalne oraz klucze symboliczne po prostu nie pojawią się w wyniku, nawet jeśli obiekt wygląda na „pełny”.
W praktyce to właśnie ten punkt psuje wiele odpowiedzi API. Programista patrzy na obiekt w debuggerze i widzi komplet danych, a klient dostaje uboższy JSON. To nie błąd transportu, tylko efekt reguł serializacji. Gdy już to rozumiem, mogę świadomie przejąć kontrolę nad wynikiem.
replacer, space i toJSON() pozwalają przejąć kontrolę nad wynikiem
Jeśli zwykłe JSON.stringify() daje za dużo albo za mało, do gry wchodzą trzy mechanizmy: replacer, space i toJSON(). Każdy służy do czegoś innego, więc dobrze je rozdzielić, zamiast traktować jak jeden „magiczny” trik.
replacer przydaje się wtedy, gdy chcesz filtrować pola albo przekształcić konkretne wartości. W wersji tablicowej wybierasz tylko wskazane klucze, a w wersji funkcyjnej możesz dynamicznie ukrywać dane, np. tokeny, hasła lub pola techniczne.
const user = {
id: 12,
name: "Anna",
token: "sekret",
};
const safeJson = JSON.stringify(user, ["id", "name"]);
W tym przypadku do wyniku przechodzą tylko dwa pola. To prosty sposób na redakcję danych, ale ma jedną wadę: łatwo za bardzo przyciąć payload i przypadkiem wyciąć coś potrzebnego w API.
space odpowiada za czytelność. W logach, debugowaniu i testach snapshotowych jest bardzo wygodny, ale w normalnej odpowiedzi produkcyjnej zwykle szkoda na niego miejsca. MDN podaje przy tym praktyczne ograniczenie: liczba odstępów jest ucinana do 10, więc większe wartości nie mają sensu.
JSON.stringify({ a: 1, b: { c: 2 } }, null, 2);
toJSON() daje jeszcze większą kontrolę, bo obiekt sam może powiedzieć, jak ma wyglądać po serializacji. Z tego powodu Date trafia do JSON jako ISO string, a nie jako wewnętrzna reprezentacja czasu. To wygodne, ale trzeba pamiętać, że takie zachowanie zmienia kontrakt danych i powinno być świadome, a nie przypadkowe.
Właśnie dlatego traktuję te trzy mechanizmy jak narzędzia precyzyjne, a nie ozdobniki. Dają kontrolę, ale też łatwo nimi zamaskować problem zamiast go rozwiązać. A kiedy kontrakt już działa, najczęściej wychodzą błędy wdrożeniowe, które można było wyłapać wcześniej.
Najczęstsze błędy w backendzie i API
W pracy z API widzę kilka powtarzalnych potknięć. Zwykle nie wynikają z braku znajomości składni, tylko z tego, że serializacja bywa „załatwiana po drodze” i nikt nie sprawdza efektu końcowego.
-
Podwójne stringify - jeśli robisz
res.json(JSON.stringify(data)), często kończysz z tekstem w tekście. Klient dostaje string, a nie obiekt. -
Mylenie
undefinedznull- w JSON to nie jest to samo. Brak pola i jawnenullmają inne znaczenie dla walidacji oraz logiki biznesowej. - Wysyłanie całych encji domenowych - obiekt z ORM potrafi zawierać metody, relacje i pola techniczne, których API nie powinno ujawniać.
-
Ignorowanie cykli - relacje rodzic-dziecko albo referencje do nadrzędnych obiektów szybko kończą się błędem
TypeError. -
Zakładanie, że każda struktura działa jak plain object -
Map,Seti niektóre klasy wymagają jawnej konwersji.
Najbardziej kosztowny jest pierwszy błąd, bo wygląda niewinnie. Jeśli framework już serializuje odpowiedź za ciebie, dodatkowe stringowanie zwykle tylko komplikuje format i utrudnia debugowanie po stronie klienta.
Drugi częsty problem to bezpieczeństwo. Gdy wysyłam na zewnątrz zbyt bogaty obiekt, łatwo przypadkiem ujawnić dane, które miały zostać tylko w backendzie. W praktyce lepszy jest kontrolowany DTO niż „wszystko, co jest pod ręką”. Żeby to działało dobrze, trzeba też ustawić sam przepływ danych między frontendem a serwerem.
Jak zbudować bezpieczny przepływ danych między frontendem a backendem
Gdy patrzę na API z perspektywy całego przepływu, najważniejsze jest nie samo wywołanie funkcji, ale kontrakt danych. Frontend powinien wysyłać dokładnie to, czego backend oczekuje, a backend powinien odpowiadać strukturą, którą klient potrafi przewidzieć.
Dobry wzorzec wygląda tak:
- Na froncie tworzę prosty obiekt wejściowy bez zbędnych pól technicznych.
- Przed żądaniem HTTP zamieniam go na JSON przez
JSON.stringify(). - Wysyłam go z nagłówkiem
Content-Type: application/json. - Po stronie serwera parsuję body i od razu mapuję dane do własnego modelu lub DTO.
- Przed odpowiedzią filtruję sekrety, dane wewnętrzne i pola, których klient nie powinien znać.
await fetch("/api/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
productId: 42,
quantity: 2,
}),
});
Ten fragment wygląda banalnie, ale właśnie tu najczęściej wygrywa prostota. Jeśli payload jest mały i czytelny, backend łatwiej go waliduje, a logi są mniej hałaśliwe.
Jeżeli potrzebuję debugować cały obieg, nie zwiększam bezmyślnie zagnieżdżenia czytelności. Najpierw sprawdzam, czy odpowiedź API zawiera dokładnie te pola, które powinny przejść przez granicę systemu. To oszczędza więcej czasu niż późniejsze polowanie na „znikające” wartości. Kiedy ten fundament jest dobrze ustawiony, zostaje już tylko kilka decyzji produkcyjnych, które domykają temat.
Co warto zapamiętać, zanim wdrożysz serializację na produkcji
Najważniejsza zasada jest prosta: serializuj świadomie, a nie przy okazji. Jeśli zostawisz to przypadkowi, prędzej czy później trafisz na brakujące pola, błąd przy BigInt albo nieczytelny format odpowiedzi.
- Ustal, które pola mają prawo opuścić backend, zanim zaczniesz serializować obiekt.
- Traktuj
Date,BigInt,MapiSetjako przypadki wymagające jawnej decyzji. - Używaj
replacerdo filtrowania, ale nie do ukrywania złego modelu danych. -
spacezostaw do debugowania, logów i testów, nie do standardowej odpowiedzi API. - Jeśli odpowiedź ma być przewidywalna, buduj mały DTO zamiast serializować złożony obiekt domenowy.
W praktyce właśnie takie podejście daje najlepszy efekt: prosty kontrakt, przewidywalny JSON i mniej niespodzianek po stronie klienta. A kiedy ten fundament jest dobrze ustawiony, JSON.stringify() przestaje być tylko funkcją z dokumentacji i staje się jednym z najważniejszych elementów pracy z backendem i API.