Формат JSON, метод toJSON

В этой главе мы рассмотрим работу с форматом JSON, который используется для представления объектов в виде строки.

Это один из наиболее удобных форматов данных при взаимодействии с JavaScript. Если нужно с сервера взять объект с данными и передать на клиенте, то в качестве промежуточного формата – для передачи по сети, почти всегда используют именно его.

В современных браузерах есть замечательные методы, знание тонкостей которых делает операции с JSON простыми и комфортными.

Формат JSON

Данные в формате JSON (RFC 4627) представляют собой:

  • JavaScript-объекты { ... } или
  • Массивы [ ... ] или
  • Значения одного из типов:
    • строки в двойных кавычках,
    • число,
    • логическое значение true/false,
    • null.

Почти все языки программирования имеют библиотеки для преобразования объектов в формат JSON.

Основные методы для работы с JSON в JavaScript – это:

  • JSON.parse – читает объекты из строки в формате JSON.
  • JSON.stringify – превращает объекты в строку в формате JSON, используется, когда нужно из JavaScript передать данные по сети.

Метод JSON.parse

Вызов JSON.parse(str) превратит строку с данными в формате JSON в JavaScript-объект/массив/значение.

Например:

var numbers = "[0, 1, 2, 3]";

numbers = JSON.parse(numbers);

alert( numbers[1] ); // 1

Или так:

var user = '{ "name": "Вася", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }';

user = JSON.parse(user);

alert( user.friends[1] ); // 1

Данные могут быть сколь угодно сложными, объекты и массивы могут включать в себя другие объекты и массивы. Главное чтобы они соответствовали формату.

JSON-объекты ≠ JavaScript-объекты

Объекты в формате JSON похожи на обычные JavaScript-объекты, но отличаются от них более строгими требованиями к строкам – они должны быть именно в двойных кавычках.

В частности, первые два свойства объекта ниже – некорректны:

{
  name: "Вася",       // ошибка: ключ name без кавычек!
  "surname": 'Петров',// ошибка: одинарные кавычки у значения 'Петров'!
  "age": 35,           // .. а тут всё в порядке.
  "isAdmin": false    // и тут тоже всё ок
}

Кроме того, в формате JSON не поддерживаются комментарии. Он предназначен только для передачи данных.

Есть нестандартное расширение формата JSON, которое называется JSON5 и как раз разрешает ключи без кавычек, комментарии и т.п, как в обычном JavaScript. На данном этапе, это отдельная библиотека.

Умный разбор: JSON.parse(str, reviver)

Метод JSON.parse поддерживает и более сложные алгоритмы разбора.

Например, мы получили с сервера объект с данными события event.

Он выглядит так:

// title: название события, date: дата события
var str = '{"title":"Конференция","date":"2014-11-30T12:00:00.000Z"}';

…И теперь нужно восстановить его, то есть превратить в JavaScript-объект.

Попробуем вызвать для этого JSON.parse:

var str = '{"title":"Конференция","date":"2014-11-30T12:00:00.000Z"}';

var event = JSON.parse(str);

alert( event.date.getDate() ); // ошибка!

…Увы, ошибка!

Дело в том, что значением event.date является строка, а отнюдь не объект Date. Откуда методу JSON.parse знать, что нужно превратить строку именно в дату?

Для интеллектуального восстановления из строки у JSON.parse(str, reviver) есть второй параметр reviver, который является функцией function(key, value).

Если она указана, то в процессе чтения объекта из строки JSON.parse передаёт ей по очереди все создаваемые пары ключ-значение и может возвратить либо преобразованное значение, либо undefined, если его нужно пропустить.

В данном случае мы можем создать правило, что ключ date всегда означает дату:

// дата в строке - в формате UTC
var str = '{"title":"Конференция","date":"2014-11-30T12:00:00.000Z"}';

var event = JSON.parse(str, function(key, value) {
  if (key == 'date') return new Date(value);
  return value;
});

alert( event.date.getDate() ); // теперь сработает!

Кстати, эта возможность работает и для вложенных объектов тоже:

var schedule = '{ \
  "events": [ \
    {"title":"Конференция","date":"2014-11-30T12:00:00.000Z"}, \
    {"title":"День рождения","date":"2015-04-18T12:00:00.000Z"} \
  ]\
}';

schedule = JSON.parse(schedule, function(key, value) {
  if (key == 'date') return new Date(value);
  return value;
});

alert( schedule.events[1].date.getDate() ); // сработает!

Сериализация, метод JSON.stringify

Метод JSON.stringify(value, replacer, space) преобразует («сериализует») значение в JSON-строку.

Пример использования:

var event = {
  title: "Конференция",
  date: "сегодня"
};

var str = JSON.stringify(event);
alert( str ); // {"title":"Конференция","date":"сегодня"}

// Обратное преобразование.
event = JSON.parse(str);

При сериализации объекта вызывается его метод toJSON.

Если такого метода нет – перечисляются его свойства, кроме функций.

Посмотрим это в примере посложнее:

var room = {
  number: 23,
  occupy: function() {
    alert( this.number );
  }
};

event = {
  title: "Конференция",
  date: new Date(Date.UTC(2014, 0, 1)),
  room: room
};

alert( JSON.stringify(event) );
/*
  {
    "title":"Конференция",
    "date":"2014-01-01T00:00:00.000Z",  // (1)
    "room": {"number":23}               // (2)
  }
*/

Обратим внимание на два момента:

  1. Дата превратилась в строку. Это не случайно: у всех дат есть встроенный метод toJSON. Его результат в данном случае – строка в таймзоне UTC.

  2. У объекта room нет метода toJSON. Поэтому он сериализуется перечислением свойств.

    Мы, конечно, могли бы добавить такой метод, тогда в итог попал бы его результат:

    var room = {
      number: 23,
      toJSON: function() {
          return this.number;
        }
    };
    
    alert( JSON.stringify(room) ); // 23

Исключение свойств

Попытаемся преобразовать в JSON объект, содержащий ссылку на DOM.

Например:

var user = {
  name: "Вася",
  age: 25,
  window: window
};

alert( JSON.stringify(user) ); // ошибка!
// TypeError: Converting circular structure to JSON (текст из Chrome)

Произошла ошибка! В чём же дело, неужели некоторые объекты запрещены? Как видно из текста ошибки – дело совсем в другом. Глобальный объект window – сложная структура с кучей встроенных свойств и круговыми ссылками, поэтому его преобразовать невозможно. Да и нужно ли?

Во втором параметре JSON.stringify(value, replacer) можно указать массив свойств, которые подлежат сериализации.

Например:

var user = {
  name: "Вася",
  age: 25,
  window: window
};

alert( JSON.stringify(user, ["name", "age"]) );
// {"name":"Вася","age":25}

Для более сложных ситуаций вторым параметром можно передать функцию function(key, value), которая возвращает сериализованное value либо undefined, если его не нужно включать в результат:

var user = {
  name: "Вася",
  age: 25,
  window: window
};

var str = JSON.stringify(user, function(key, value) {
  if (key == 'window') return undefined;
  return value;
});

alert( str ); // {"name":"Вася","age":25}

В примере выше функция пропустит свойство с названием window. Для остальных она просто возвращает значение, передавая его стандартному алгоритму. А могла бы и как-то обработать.

Функция replacer работает рекурсивно

То есть, если объект содержит вложенные объекты, массивы и т.п., то все они пройдут через replacer.

Красивое форматирование

В методе JSON.stringify(value, replacer, space) есть ещё третий параметр space.

Если он является числом – то уровни вложенности в JSON оформляются указанным количеством пробелов, если строкой – вставляется эта строка.

Например:

var user = {
  name: "Вася",
  age: 25,
  roles: {
    isAdmin: false,
    isEditor: true
  }
};

var str = JSON.stringify(user, "", 4);

alert( str );
/* Результат -- красиво сериализованный объект:
{
    "name": "Вася",
    "age": 25,
    "roles": {
        "isAdmin": false,
        "isEditor": true
    }
}
*/

Итого

  • JSON – формат для представления объектов (и не только) в виде строки.
  • Методы JSON.parse и JSON.stringify позволяют интеллектуально преобразовать объект в строку и обратно.

Задачи

важность: 3

Превратите объект leader из примера ниже в JSON:

var leader = {
  name: "Василий Иванович",
  age: 35
};

После этого прочитайте получившуюся строку обратно в объект.

var leader = {
  name: "Василий Иванович",
  age: 35
};

var leaderStr = JSON.stringify(leader);
leader = JSON.parse(leaderStr);
важность: 3

Превратите объект team из примера ниже в JSON:

var leader = {
  name: "Василий Иванович"
};

var soldier = {
  name: "Петька"
};

// эти объекты ссылаются друг на друга!
leader.soldier = soldier;
soldier.leader = leader;

var team = [leader, soldier];
  1. Может ли это сделать прямой вызов JSON.stringify(team)? Если нет, то почему?
  2. Какой подход вы бы предложили для чтения и восстановления таких объектов?

Ответ на первый вопрос

Обычный вызов JSON.stringify(team) выдаст ошибку, так как объекты leader и soldier внутри структуры ссылаются друг на друга.

Формат JSON не предусматривает средств для хранения ссылок.

Варианты решения

Чтобы превращать такие структуры в JSON, обычно используются два подхода:

  1. Добавить в team свой код toJSON:

    team.toJSON = function() {
      /* свой код, который может создавать копию объекта без круговых ссылок и передавать управление JSON.stringify */
    }

    При этом, конечно, понадобится и своя функция чтения из JSON, которая будет восстанавливать объект, а затем дополнять его круговыми ссылками.

  2. Можно учесть возможную проблему в самой структуре, используя вместо ссылок id. Как правило, это несложно, ведь на сервере у данных тоже есть идентификаторы.

    Изменённая структура может выглядеть так:

    var leader = {
      id: 12,
      name: "Василий Иванович"
    };
    
    var soldier = {
      id: 51,
      name: "Петька"
    };
    
    // поменяли прямую ссылку на ID
    leader.soldierId = 51;
    soldier.leaderId = 12;
    
    var team = {
      12: leader,
      51: soldier
    };

    …Но действительно ли это решение будет оптимальным? Использовать структуру стало сложнее, и вряд ли это изменение стоит делать лишь из-за JSON. Вот если есть другие преимущества, тогда можно подумать.

Универсальный вариант подхода, описанного выше – это использование особой реализации JSON, которая не входит в стандарт и поддерживает расширенный формат для поддержки ссылок.

Она, к примеру, есть во фреймворке Dojo.

При вызове dojox.json.ref.toJson(team) будет создано следующее строковое представление:

[{"name":"Василий Иванович","soldier":{"name":"Петька","leader":{"$ref":"#0"}}},{"$ref":"#0.soldier"}]

Метод разбора такой строки – также свой: dojox.json.ref.fromJson.

Карта учебника

Комментарии

перед тем как писать…
  • Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.