Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

JSON em JavaScript: JSON.parse e JSON.stringify

As regras que o JSON não perdoa, o que o stringify descarta em silêncio e como ler os dois erros mais comuns: token < inesperado e estrutura circular.

Rodolfo Mori7 min de leitura

JSON é um formato de texto para transportar dados. JSON.stringify pega um valor JavaScript e devolve a string; JSON.parse pega a string e devolve um valor novo. As duas funções vivem no objeto global JSON e não precisam de importação nenhuma, nem no navegador nem no Node.

Resposta curta: stringify serializa, parse desserializa, e a volta nunca é idêntica à ida. Data vira texto, undefined some, função some, e o objeto que volta é outro objeto — com os mesmos valores, mas sem nenhuma ligação com o original.

Os nomes técnicos são serialização e desserialização. Em palavras simples, stringify empacota certos dados como texto e parse monta um novo valor a partir desse texto. Não é pausa do objeto nem cópia perfeita de tudo.

A mudança para caixas: o que cabe e o que fica para trás

Numa mudança, você coloca em caixas o que pode ser transportado, escreve etiquetas e remonta o ambiente no destino. A casa original não viaja inteira: alguns objetos ficam, e a montagem nova não é o mesmo endereço. JSON faz esse transporte com os tipos que o formato conhece; função e undefined ficam para trás, enquanto data perde o tipo e vira texto.

Antes do primeiro exemplo, liste tipo, valor e identidade esperados depois da ida e volta. Execute e confira com typeof, igualdade de valor e comparação por referência. Assim você verifica três propriedades diferentes em vez de resumir tudo a “o objeto voltou”.

js
const pedido = {
  numero: 8412,
  cliente: 'Ana Souza',
  itens: [{ sku: 'TEC-01', preco: 289.9, quantidade: 1 }],
  freteGratis: true,
};

const texto = JSON.stringify(pedido);
console.log(texto);
console.log(typeof texto, texto.length);

const devolta = JSON.parse(texto);
console.log(devolta.itens[0].sku, devolta.freteGratis);
console.log(devolta === pedido);
{"numero":8412,"cliente":"Ana Souza","itens":[{"sku":"TEC-01","preco":289.9,"quantidade":1}],"freteGratis":true} string 112 TEC-01 true false

A última linha é o ponto que mais gera confusão: devolta tem o mesmo conteúdo, mas é um objeto novo. Nada que você mudar nele afeta pedido. É por isso que a dupla parse(stringify(x)) virou um clone de pobre — e por que ela é uma má ideia: tudo que o stringify descarta (Date, undefined, função, NaN) some no caminho, e uma referência circular derruba o clone com exceção.

Para leitura humana, o terceiro argumento define a indentação:

js
const pedido = {
  numero: 8412,
  cliente: 'Ana Souza',
  endereco: { cidade: 'São Paulo', uf: 'SP' },
};

console.log(JSON.stringify(pedido, null, 2));
{ "numero": 8412, "cliente": "Ana Souza", "endereco": { "cidade": "São Paulo", "uf": "SP" } }

O que o JSON não aceita

A sintaxe parece a de objeto JavaScript, mas é bem mais estreita:

em JavaScript vale em JSON
{ numero: 8412 } proibido — chave sempre entre aspas duplas
{ 'cliente': 'Ana' } proibido — aspas simples não existem
{ "a": 1, } proibido — vírgula final derruba o parse
// comentário proibido — não existe comentário em JSON
{ "total": 289.9 } válido

Só seis tipos entram: string, número, booleano, null, objeto e array. Não existe data, não existe undefined, não existe NaN, não existe função.

O que o stringify descarta em silêncio

Esta é a parte que pega gente experiente, porque não há erro nenhum — os campos simplesmente somem:

js
const pedido = {
  numero: 8412,
  cupom: undefined,
  calcularTotal() {
    return 289.9;
  },
  [Symbol('interno')]: 'some',
  observacao: null,
};

console.log(JSON.stringify(pedido));

const historico = [8412, undefined, () => 1, Symbol('x'), 8413];
console.log(JSON.stringify(historico));

console.log(JSON.stringify({ desconto: NaN, frete: Infinity }));
{"numero":8412,"observacao":null} [8412,null,null,null,8413] {"desconto":null,"frete":null}

Três comportamentos diferentes numa saída só:

  • Em objeto, undefined, função e Symbol são omitidos — a chave nem aparece. null fica, porque null é um valor válido em JSON.
  • Em array, omitir mudaria os índices, então eles viram null. A posição é preservada, o valor não.
  • NaN e Infinity também viram null. Se o seu cálculo produziu NaN, o JSON não vai te avisar: ele manda null para o servidor.

Data vira texto ISO — e não volta sozinha

js
const pedido = {
  numero: 8412,
  criadoEm: new Date('2026-03-15T14:30:00-03:00'),
};

const texto = JSON.stringify(pedido);
console.log(texto);

const devolta = JSON.parse(texto);
console.log(typeof devolta.criadoEm);
console.log(devolta.criadoEm.getFullYear);
{"numero":8412,"criadoEm":"2026-03-15T17:30:00.000Z"} string undefined

Duas coisas aconteceram. O horário virou UTC — 14:30 em Brasília é 17:30Z, e esse deslocamento é assunto da lição sobre datas em JavaScript. E o que voltou foi uma string: getFullYear nem existe nela. Chamar um método de Date num valor desses produz o velho is not a function.

O segundo argumento do parse conserta isso na entrada:

js
const texto = '{"numero":8412,"criadoEm":"2026-03-15T17:30:00.000Z"}';

const pedido = JSON.parse(texto, (chave, valor) => {
  if (chave === 'criadoEm' && typeof valor === 'string') return new Date(valor);
  return valor;
});

console.log(pedido.criadoEm instanceof Date);
console.log(pedido.criadoEm.toLocaleDateString('pt-BR'));
true 15/03/2026

Essa função é o reviver: ela é chamada para cada par chave/valor e o que ela devolve é o que entra no objeto final. Repare que eu testei o nome da chave em vez de sair convertendo toda string que pareça uma data — regex genérica de ISO tem o hábito de transformar código de rastreio em data.

replacer: escolher e transformar na saída

O segundo argumento do stringify faz o mesmo trabalho na direção contrária. Ele aceita uma lista de chaves permitidas ou uma função:

js
const cliente = {
  nome: 'Ana Souza',
  email: 'ana@exemplo.com.br',
  senha: 'nao-vai-para-o-log',
  cpf: '123.456.789-00',
  total: 289.94999,
};

console.log(JSON.stringify(cliente, ['nome', 'email']));

console.log(
  JSON.stringify(cliente, (chave, valor) => {
    if (chave === 'senha' || chave === 'cpf') return undefined;
    if (chave === 'total') return Number(valor.toFixed(2));
    return valor;
  }),
);
{"nome":"Ana Souza","email":"ana@exemplo.com.br"} {"nome":"Ana Souza","email":"ana@exemplo.com.br","total":289.95}

Devolver undefined na função apaga a chave — é a mesma regra do descarte que vimos acima, agora usada de propósito. É o jeito mais direto de garantir que senha e CPF não vazem para um log.

toJSON: o objeto decide como se serializa

Se um valor tem um método toJSON, o stringify chama esse método e serializa o que ele devolver. Date faz exatamente isso — e a sua classe pode fazer também:

js
class Pedido {
  #chaveDoGateway = 'sk_live_naovaipro_json';

  constructor(numero, cliente, total) {
    this.numero = numero;
    this.cliente = cliente;
    this.total = total;
  }

  toJSON() {
    return {
      numero: this.numero,
      cliente: this.cliente,
      total: Number(this.total.toFixed(2)),
    };
  }
}

const pedido = new Pedido(8412, 'Ana Souza', 289.94999);
console.log(JSON.stringify(pedido));
console.log(JSON.stringify({ ultimo: pedido }));
{"numero":8412,"cliente":"Ana Souza","total":289.95} {"ultimo":{"numero":8412,"cliente":"Ana Souza","total":289.95}}

A segunda linha mostra que a regra vale em qualquer profundidade: o objeto aninhado também passou pelo próprio toJSON. Campo privado, aliás, nunca sairia mesmo — mas com toJSON você controla o formato inteiro em vez de torcer.

Erros comuns

Aspas simples e vírgula final

js
const texto = "{'numero': 8412, 'cliente': 'Ana Souza'}";

console.log(JSON.parse(texto));
<anonymous_script>:1 {'numero': 8412, 'cliente': 'Ana Souza'} ^ SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2) at JSON.parse (<anonymous>) at file:///private/tmp/checkout/json-aspas-simples.mjs:3:18 Node.js v24.16.0
js
const texto = `{
  "numero": 8412,
  "cliente": "Ana Souza",
}`;

console.log(JSON.parse(texto));
<anonymous_script>:4 } ^ SyntaxError: Expected double-quoted property name in JSON at position 46 (line 4 column 1) at JSON.parse (<anonymous>) at file:///private/tmp/checkout/json-virgula-final.mjs:6:18 Node.js v24.16.0

Repare no cabeçalho: o Node não mostra o seu arquivo primeiro, e sim <anonymous_script> — o texto que estava sendo lido, com o cursor no caractere que travou. O } da última linha só ficou errado porque a vírgula anterior prometeu mais uma chave. E a mensagem ainda dá linha, coluna e posição: com elas, texto.slice(40, 60) mostra a vizinhança do problema na hora.

Unexpected token '<' — você não recebeu JSON

O erro mais comum de todos não é de JSON malformado. É de resposta que nunca foi JSON:

js
const resposta = `<!DOCTYPE html>
<html lang="pt-BR">
  <head><title>502 Bad Gateway</title></head>
</html>`;

const pedido = JSON.parse(resposta);
console.log(pedido.numero);
<anonymous_script>:1 <!DOCTYPE html> ^ SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON at JSON.parse (<anonymous>) at file:///private/tmp/checkout/json-recebeu-html.mjs:6:21 Node.js v24.16.0

O cursor aponta para o primeiro caractere do corpo, e o Node ainda imprime a linha inteira: <!DOCTYPE html>. É o começo de uma página HTML — erro de gateway, tela de login, página 404 do servidor. A URL está errada, a sessão caiu ou o serviço está fora. Não adianta mexer no parse: o problema é a requisição.

O diagnóstico leva dez segundos:

js
const resposta = `<!DOCTYPE html>
<html lang="pt-BR">
  <head><title>502 Bad Gateway</title></head>
</html>`;

try {
  JSON.parse(resposta);
} catch (erro) {
  console.log('não era JSON. Os 60 primeiros caracteres do corpo:');
  console.log(resposta.slice(0, 60));
  console.log('mensagem:', erro.message);
}
não era JSON. Os 60 primeiros caracteres do corpo: <!DOCTYPE html> <html lang="pt-BR"> <head><title>502 Bad G mensagem: Unexpected token '<', "<!DOCTYPE "... is not valid JSON

Estrutura circular

JSON é uma árvore. Se o seu objeto tem um ciclo, não existe texto que o represente:

js
const cliente = { nome: 'Ana Souza' };
const pedido = { numero: 8412, cliente };

cliente.ultimoPedido = pedido;

console.log(JSON.stringify(pedido));
file:///private/tmp/checkout/json-circular.mjs:6 console.log(JSON.stringify(pedido)); ^ TypeError: Converting circular structure to JSON --> starting at object with constructor 'Object' | property 'cliente' -> object with constructor 'Object' --- property 'ultimoPedido' closes the circle at JSON.stringify (<anonymous>) at file:///private/tmp/checkout/json-circular.mjs:6:18 Node.js v24.16.0

Essa mensagem é das melhores do Node: ela desenha o caminho do ciclo, campo por campo. Aqui, pedido.cliente.ultimoPedido volta para pedido.

A saída definitiva é quebrar o ciclo no modelo de dados — guardar ultimoPedidoId em vez do objeto inteiro. Quando você só precisa logar, um replacer com WeakSet resolve:

js
const cliente = { nome: 'Ana Souza' };
const pedido = { numero: 8412, cliente };
cliente.ultimoPedido = pedido;

const vistos = new WeakSet();
const texto = JSON.stringify(pedido, (chave, valor) => {
  if (typeof valor === 'object' && valor !== null) {
    if (vistos.has(valor)) return '[circular]';
    vistos.add(valor);
  }
  return valor;
});

console.log(texto);
{"numero":8412,"cliente":{"nome":"Ana Souza","ultimoPedido":"[circular]"}}

Antes de mandar para produção

Quatro hábitos que evitam quase todo incidente com JSON:

  • Todo JSON.parse de origem externa vai dentro de try. Você não controla o que o servidor devolve, e um parse solto derruba a requisição inteira.
  • Nunca confie no formato do que voltou. Depois do parse você tem um objeto qualquer, não o seu tipo: leia os campos opcionais com ?. e ??.
  • Não use parse(stringify(x)) para clonar. Use structuredClone, que preserva Date e não morre em ciclo.
  • Serialize dinheiro com cuidado. 289.94999 só virou 289.95 porque eu arredondei no replacer; sem isso, o valor cru viaja com todas as casas.

A trilha de JavaScript segue com datas, o tipo que o JSON transforma em texto e devolve pela metade. Todo o material de JavaScript está reunido no guia da linguagem.

Prefere aprender em vídeo?

Tem uma aula sobre este assunto no nosso canal.

Ver todos os vídeos do canal
  • json
  • api
  • serializacao
  • parse
  • stringify

Perguntas frequentes

JSON e objeto JavaScript são a mesma coisa?
Não. Objeto é uma estrutura viva na memória, com métodos e referências. JSON é um formato de texto, um subconjunto restrito da sintaxe de objeto: só aspas duplas nas chaves, sem função, sem comentário e sem vírgula sobrando no fim.
Por que a data volta como string depois do JSON.parse?
Porque JSON não tem tipo data. O stringify chama toJSON na Date e grava o texto ISO; o parse não tem como saber que aquele texto era uma data. Para recuperar o objeto Date você precisa passar um reviver ou converter na mão depois.
Dá para usar JSON.parse(JSON.stringify(obj)) para clonar?
Funciona para dados simples, mas perde Date, undefined, função, Map, Set e NaN, e explode em estrutura circular. Desde o Node 17 existe structuredClone, que é mais rápido e não tem esses buracos.
Como descobrir por que um JSON.parse falhou?
Imprima os primeiros 100 caracteres do texto antes de tentar converter. Na maioria esmagadora dos casos a resposta não era JSON — era uma página de erro do proxy, um redirecionamento de login ou uma string vazia.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MDN — JSON — developer.mozilla.org
  2. MDN — JSON.stringify() — developer.mozilla.org

Continue por aqui