Некоторые тонкости методов JSON stringify и parse в JavaScript

SON stringify и parse в JavaScript
тонкости методов JSON stringify и parse в JavaScript

Методы JSON.stringify() и JSON.parse() служат для преобразования объекта в json и восстановления json в объект JS соответственно. Однако, они работают не со всеми элементами объекта корректно, а некоторые в целом игнорируются, что следует учитывать при копировании.

Здесь мы рассмотрим 2 момента, а именно невозможность перевода в JSON формат функций и символов, а так же ошибку/несоответствие при восстановлении данных на примере объекта типа Date

Введение

Возьмем объект со свойствами разных типов данных и методом:

const ticket = {
  isValid: true, // boolean
  price: 2000.0, // number
  cityFrom: "MSK", // string
  cityTo: "SPB", // string
  date: new Date(), // Date()
  *[Symbol.iterator]() {
    for (const item of Object.entries(this)) yield item;
  }, // function
};
console.dir(ticket);
// {
//     isValid: true,
//     price: 2000,
//     cityFrom: 'MSK',
//     cityTo: 'SPB',
//     date: 2024-06-27T10:05:33.522Z,
//     [Symbol(Symbol.iterator)]: [GeneratorFunction: [Symbol.iterator]]
// }

Так же нам для наглядности понадобиться функция проверки типов свойств. Для нашего объекта она выдает ожидаемые значения typeof:

function testTypes(obj) {
  for (const k in obj) console.log(k, typeof obj[k]);
}

testTypes(ticket);
// isValid boolean
// price number
// cityFrom string
// cityTo string
// date object

Стандартное преобразование

Обычно объект преобразуют напрямую, следующий код показывает сопутствующие моменты:

  • При упаковке мы потеряли метод Symbol(Symbol.iterator);
  • При восстановлении теряется тип данных даты, она становится строкой.
{
  const json = JSON.stringify(ticket);
  console.log(json);
  // json = {"isValid":true,"price":2000,"cityFrom":"MSK","cityTo":"SPB","date":"2024-06-27T10:05:33.522Z"}
  // Здесь мы встречаемся с первым моментом. Видно, что не все упаковалось, функцию мы уже потеряли.

  const parsedObject = JSON.parse(json);
  console.dir(parsedObject);
  // {
  //     isValid: true,
  //     price: 2000,
  //     cityFrom: 'MSK',
  //     cityTo: 'SPB',
  //     date: '2024-06-27T10:05:33.522Z'
  // }

  testTypes(parsedObject);
  //   Если проверить типы, что мы увидим, 
  //   что дата теперь не объект, а строка.
  //   isValid boolean
  //   price number
  //   cityFrom string
  //   cityTo string
  //   date string
}

JSON.stringify()

При упаковке, мы можем передать методу JSON.stringify второй параметр в виде массива ключей, которые надо экспортировать в json.

const json = JSON.stringify(ticket, ["isValid", "price", "cityFrom", "date"]);
console.log("json:", json);
// json: {"isValid":true,"price":2000,"cityFrom":"MSK","date":"2024-06-27T11:18:08.648Z"}
// Проблема с датой осталась, но мы
// можем фильтровать нужные данные для передачи

JSON.parse()

С методом JSON.parse все интересней, вторым аргументом можем передать функцию восстановления, с ее помощью мы сможем отслеживать интересующие нас поля и изменять их в процессе распаковки.

Пример функции восстановления для Date:

function R(k, v) {
  if (
    typeof v === "string" &&
    /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(v)
  ) {
    return new Date(v);
  }
  return v;
}

Код с функцией восстановления будет выглядеть так:

{
  const json = JSON.stringify(ticket, ["isValid", "price", "cityFrom", "date"]);
  console.log("json:", json);
  // json: {"isValid":true,"price":2000,"cityFrom":"MSK","date":"2024-06-27T11:18:08.648Z"}
  // Проблема с датой осталась, но мы
  // можем фильтровать нужные данные для передачи

  // JSON.parse с функцией восстановления
  const recoveredObject = JSON.parse(json, function (k, v) {
    if (
      typeof v === "string" &&
      /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(v)
    ) {
      return new Date(v);
    }
    return v;
  });

  console.dir(recoveredObject);
  //   {
  //     isValid: true,
  //     price: 2000,
  //     cityFrom: 'MSK',
  //     date: 2024-06-27T11:24:24.112Z
  //   }

  testTypes(recoveredObject);
  // isValid boolean
  // price number
  // cityFrom string
  // date object
}

Мы видим, что дата стала объектом, тем самым, который мы вернули в функции восстановления. Естественно ее можно расширить, для восстановления например собственных типов объекто, если у них реализован метод toJSON() и они свободно упаковываются методом JSON.stringify.

Итоговый код с комментариями

function testTypes(obj) {
  for (const k in obj) console.log(k, typeof obj[k]);
}
const ticket = {
  isValid: true, // boolean
  price: 2000.0, // number
  cityFrom: "MSK", // string
  cityTo: "SPB", // string
  date: new Date(), // Date()
  *[Symbol.iterator]() {
    for (const item of Object.entries(this)) yield item;
  }, // function
};
console.dir(ticket);
// {
//     isValid: true,
//     price: 2000,
//     cityFrom: 'MSK',
//     cityTo: 'SPB',
//     date: 2024-06-27T10:05:33.522Z,
//     [Symbol(Symbol.iterator)]: [GeneratorFunction: [Symbol.iterator]]
// }
testTypes(ticket);
// isValid boolean
// price number
// cityFrom string
// cityTo string
// date object

{
  const json = JSON.stringify(ticket);
  console.log(json);
  // json = {"isValid":true,"price":2000,"cityFrom":"MSK","cityTo":"SPB","date":"2024-06-27T10:05:33.522Z"}
  // Здесь мы встречаемся с первым моментом. Видно, что не все упаковалось, функцию мы уже потеряли.

  const parsedObject = JSON.parse(json);
  console.dir(parsedObject);
  // {
  //     isValid: true,
  //     price: 2000,
  //     cityFrom: 'MSK',
  //     cityTo: 'SPB',
  //     date: '2024-06-27T10:05:33.522Z'
  // }

  testTypes(parsedObject);
  //   Если проверить типы, что мы увидим, что дата теперь не объект, а строка.
  //   isValid boolean
  //   price number
  //   cityFrom string
  //   cityTo string
  //   date string
}

{
  const json = JSON.stringify(ticket, ["isValid", "price", "cityFrom", "date"]);
  console.log("json:", json);
  // json: {"isValid":true,"price":2000,"cityFrom":"MSK","date":"2024-06-27T11:18:08.648Z"}
  // Проблема с датой осталась, но мы
  // можем фильтровать нужные данные для передачи

  // JSON.parse с функцией восстановления
  const recoveredObject = JSON.parse(json, function (k, v) {
    if (
      typeof v === "string" &&
      /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(v)
    ) {
      return new Date(v);
    }
    return v;
  });

  console.dir(recoveredObject);
  //   {
  //     isValid: true,
  //     price: 2000,
  //     cityFrom: 'MSK',
  //     date: 2024-06-27T11:24:24.112Z
  //   }

  testTypes(recoveredObject);
  // isValid boolean
  // price number
  // cityFrom string
  // date object
}