Впервые о том, что Delphi теперь «умеет» работать с JSON я узнал после выхода Delphi 2010. Тогда в исходниках были замечены два модуля: DBXJSON.pas и DBXJSONReflect.pas. Ну, а раз именно эти два модуля появились в Delphi первыми, то с них мы и начнем разговор, но прежде, пара слов о формате JSON.
JSON — это текстовый формат обмена данными. В основе этого формате лежит всего пять понятий:
Объект — неупорядоченный набор пар «ключ/значение». Объект всегда начинается с «{» и заканчивается «}».
Массив — упорядоченный коллекция значений. Массив начинается с «[" и заканчивается "]«. Элементы массива разделяются запятыми.
Значение — строка в двойных кавычках. Значение может быть: строкой, числом, true, false, null, объектом, массивом.
Строка — набор Unicode-символов.
Число — любое число в десятичной системе счисления.
В соответствии с этими понятиями и организован механизм работы с JSON в Delphi. Открыв модуль DBXJSON.pas Вы можете обнаружить там следующие классы:
TJSONObject — объект
TJSONArray — массив
TJSONValue — значение
TJSONString — строка
TJSONNumber — число
Для работы с различными значениями JSON также предусмотрены соответствующие классы:
TJSONString — строка
TJSONNumber — число
TJSONTrue — true
TJSONFalse — false
TJSONNull — null
Для получения пары «ключ/значение» используется класс TJSONPair. От нас же требуется грамотно воспользоваться представленными выше классами и либо прочитать объект JSON, либо наоборот — записать. Этим мы сейчас и займемся.
Использование DBXJSON.pas
Чтение данных
В качестве примера работы с различными классами из модуля DBXJSON.pas разберем такой объект:
{"kind":"tasks#tasks",
"etag":"ptcy4Awv48HyN47i0A",
"items":[
{"kind":"tasks#task",
"id":"MTc4Njk1MjA4MTEyODY5OTA4",
"title":"Задача №1",
"updated":"2011-10-15T10:07:16.000Z",
"selfLink":"https://www.googleapis.com/tasks/v1/lists/MTcw/tasks/MyMTEyODY5OTA4",
"position":"00000000000000131071",
"status":"needsAction",
"due":"2011-10-14T00:00:00.000Z"},
{"kind":"tasks#task",
"id":"MTc4Njk1MjA4NTYyMTMzMDA",
"title":"Задача №2",
"updated":"2011-10-15T09:41:50.000Z",
"selfLink":"https://www.googleapis.com/tasks/v1/lists/MTcw/tasks/MTc4Njk1MjA4MTMzMDA",
"position":"00000000000000196607",
"status":"completed",
"completed":"2011-10-15T09:41:50.000Z"}
]
}
Постоянные читатели блога могут сразу догадаться почему именно такой объект я выбрал в качестве примера. Это JSON-объект из
Google Tasks API, содержащий список задач. Вначале посмотрим из чего состоит объект.
Вначале объекта идут две пары, содержащие служебную информацию по ответу — kind, значение которого говорит нам, что мы запросили список задач и Etag - идентификатор ответа. Далее следует массив items в котором каждый элемент — это объект, содержащий сведения по отдельной задаче.
При этом следует обратить внимание на то, что каждый JSON-объект задачи может состоять из различных пар.В примере выше Вы можете видеть, что «Задача №1″ содержит сведения по сроку выполнения задачи («due«), в то время как «Задача №2″ не имеет срока выполнения, но зато отмечена как выполненная («completed«).
Приложение для парсинга JSON
Чтобы наиболее полно охватить работу с DBXJSON.pas, используя предложенный выше JSON-объект, вначале напишем небольшое приложение, которое будет парсить данные из файла, содержащего JSON-объект и «рассказывать» нам о том, что содержится в JSON-объекте и «подсказывать» какой класс будет использоваться для парсинга значений из той или иной пары. Для этого сохраним объект, предложенный выше в текстовый файл с названием testObject.txt, создадим новый проект Delphi-приложения и на главной форме разместим следующие компоненты:
Работа программы будет происходить следующим образом:
при открытии очередного файла пробуем разобрать, содержащиеся в файле данные
если удалось провести парсинг данных из файла, то записываем в ComboBox имена пар, иначе — сообщаем пользователю о том, что файл не содержит JSON-данных
при выборе значения в ComboBox’e выводим значение пары в виде строки в Memo и определяем класс, который необходимо использовать для парсинга значения пары.
Вначале напишем обработчик OnClick кнопки:
type
Tfmain = class(TForm)
[...]
private
FJSONObject: TJSONObject;
public
end;
procedure Tfmain.Button1Click(Sender: TObject);
var S: TStringList;
begin
if OpenDialog1.Execute then
begin
lbPathToFile.Caption:=OpenDialog1.FileName;
S:=TStringList.Create;
try
//читаем файл
S.LoadFromFile(OpenDialog1.FileName);
//пробуем распарсить
FJSONObject:=TJSONObject.ParseJSONValue(S.Text) as TJSONObject;
if Assigned(FJSONObject) then //парсинг прошел успешно - считываем названия пар
EnumPairs
else //парсинг провалился - выводим сообщение
raise Exception.Create('Файл не содержит JSON-данные');
finally
S.Free;
end;
end;
end;
Здесь мы воспользовались одним из четырех перегруженных классовых методом у TJSONObject для получения JSON-объекта и передали в метод строку. Также мы могли бы воспользоваться следующими методами для парсинга:
class function ParseJSONValue(const Data: TBytes; const Offset: Integer; IsUTF8: Boolean = True): TJSONValue; overload; static;
class function ParseJSONValue(const Data: TBytes; const Offset: Integer; const Count: Integer; IsUTF8: Boolean = True): TJSONValue; overload; static;
class function ParseJSONValue(const Data: String): TJSONValue; overload; static;
class function ParseJSONValue(const Data: UTF8String): TJSONValue; overload; static;
Как видите в качестве параметров метода можно задать массив байт (TBytes), Unicode-строку или UTF8-строку. Я решил воспользоваться самым простым вариантом — использовать Unicode-строку и читать файл с самого начала без сдвигов (параметр Offset в методах).
Следующий наш шаг — чтение названий пар. Для этого в программе реализован метод EnumPairs, который выглядит следующим образом:
procedure Tfmain.EnumPairs;
var i:integer;
begin
cbPairs.Clear;
for I := 0 to FJSONObject.Size-1 do
cbPairs.Items.Add(FJSONObject.Get(i).JsonString.Value);
end;
Рассмотрим, что здесь делается.
Во-первых, мы воспользовались свойством Size объекта FJSONObject, которое возвращает нам количество пар, содержащихся в объекте.
Во-вторых, мы воспользовались методом Get, который возвращает нам отдельную пару (объект типа TJSONPair). В TJSONObject содержится два перегруженных метода Get первый из которых возвращает пару по её номеру, второй — по имени:
function Get(const I: Integer): TJSONPair; overload;
function Get(const PairName: UnicodeString): TJSONPair; overload;
Так как мы не можем знать какой объект содержится в файле и какие пары есть в объекте, то воспользовались первым вариантом Get — с использованием индексов.
В-третьих, после того, как метод Get вернул нам пару, мы, используя свойство JSONString типа TJSONString прочитали имя пары (свойство Value) и добавили его в ComboBox.
Следующий шаг — вывод строкового значения пары в Memo и определение класса для парсинга этого значения. Для этого напишем обработчик OnChange у ComboBox:
procedure Tfmain.cbPairsChange(Sender: TObject);
begin
mmPairValue.Clear;
mmPairValue.Text:=FJSONObject.Get(cbPairs.ItemIndex).JsonValue.ToString;
lbClass.Caption:=FJSONObject.Get(cbPairs.ItemIndex).JsonValue.ClassName
end;
Здесь мы воспользовались вторым свойством пары TJSONPair — JSONValue, которое в отличие от JSONString имеет тип TJSONValue. При этом следует обратить внимание на следующий момент:
Имя пары — это всегда какая-либо строка, в то время как значение может быть и строкой и массивом и числом и т.д. Поэтому для вывода строкового значения пары я воспользовался именно методом ToString, а не использовал свойство TJSONValue.Value, которое вернуло бы мне вместо значения пустую строку, в случае, если бы в значении содержался бы, например, массив.
Ну, а далее, имея значение пары мы просто вывели в Label имя класса для JSONValue.
Вот и вся программа. Теперь запустим программу и проверим работу на нашем объекте, который, как Вы помните, мы сохранили в файл TestObject.txt. Скрин работающей программы представлен ниже:
Чтение свойств класса из JSON-объекта
Теперь, имя под рукой наше приложение, мы можем легко определить какой класс надо использовать для парсинга значений той или иной пары, а значит можем вернуться к нашему объекту со списком заданий и распарсить его вдоль и поперек. Для примера создадим класс, содержащий все возможные свойства для задания из Google Tasks (какие это свойства можно глянуть в
документации к API). У меня класс получился следующий:
TTaskResource = class()
private
FId: string;
FTitle: string;
FUpdated: TDateTime;
FSelfLink: string;
FParent: string;
FPosition: string;
FNotes: string;
FStatus: string;
FDue: TDateTime;
FCompleted: TDateTime;
FDeleted: string;
FHidden: string;
procedure SetCompleted(const Value: TDateTime);
procedure SetDue(const Value: TDateTime);
procedure SetId(const Value: string);
procedure SetNotes(const Value: string);
procedure SetStatus(const Value: string);
procedure SetTitle(const Value: string);
procedure ParseJSON(Value: TJSONValue);
public
constructor Create;
property Id: string read FId;
property Title: string read FTitle write SetTitle;
property Updated: TDateTime read FUpdated;
property SelfLink: string read FSelfLink;
property Parent: string read FParent;
property Position: string read FPosition;
property Notes: string read FNotes write SetNotes;
property Status: string read FStatus write SetStatus;
property Due: TDateTime read FDue write SetDue;
property Completed: TDateTime read FCompleted write SetCompleted;
property Deleted: string read FDeleted;
property Hidden: string read FHidden;
end;
Как уже было сказано выше, объект, содержащий свойства задания может состоять из различного количества пар в зависимости от того, какое задание передается (завершенное или нет, со сроком выполнения или нет и т.д.). В связи с этим метод ParseJSON должен не просто выбирать пары по их именам, а по примеру нашей программы считывать пары по их индексам и уже после этого определять что это за пара и заполнять соответствующее поле. Метод ParseJSON можно представить так:
procedure TTaskResource.ParseJSON(Value: TJSONValue);
var
JObject: TJSONObject;
JPair: TJSONPair;
i:integer;
MemberName: string;
begin
if not Assigned(Value) then Exit;
JObject := (Value as TJSONObject);//привели значение пары к классу TJSONObject
try
{проходим по каждой паре}
for I := 0 to JObject.Size-1 do
begin
JPair:=JObject.Get(i);//получили пару по её индексу
MemberName:=JPair.JsonString.Value;//определили имя
{ищем в какое свойство записывать значение}
if MemberName=cKindTag then
FKind:=JPair.JsonValue.Value
else
[...]
else
if MemberName=cDeletedTag then
FDeleted:=JPair.JsonValue.Value
else
if MemberName=cHiddenTag then
FHidden:=JPair.JsonValue.Value;
end;
except
raise Exception.Create('Ошибка разбора JSON');
end;
end;
Здесь мы уже без лишних проверок используем свойство Value у TJSONValue, т.к. мы знаем, что за класс содержится в значении каждой пары (это TJSONString). Двигаемся далее. Процедура выше — это только парсинг пар в известном объекте. Но наш главный объект для парсинга содержит целый массив таких классов (см. выше листинг объекта). Поэтому следует предусмотреть работу с JSON-массивом.
Для работы с JSON-массивами в модуле DBXJSON.pas предусмотрено два класса:
TJSONArray — класс массива
TJSONArrayEnumerator — перечислитель элементов массива.
Рассмотрим как использовать оба этих класса в работе с JSON-массивом на примере всё того же
JSON-объекта со списком задач Google. Вначале пример использования TJSONArray без использования класса-перечислителя:
procedure Tfmain.EnumArray;
var JsonArray: TJSONArray;
i: integer;
begin
{получаем масиив из значения пары с названием "items"}
JsonArray:=FJSONObject.Get('items').JsonValue as TJSONArray;
for I := 0 to JsonArray.Size-1 do
ShowMessage((JsonArray.Get(i) as TJSONObject).Get('title').JsonValue.Value)
end;
Так как мы уже знаем, что массив содержится в «items», то в приведенной выше процедуре мы воспользовались перегруженным методом Get у TJSONObject и получили массив из значения пары по её названию. После того как метод Get вернул нам значение TJSONValue мы привели это значение к типу TJSONArray и тем самым получили возможность использовать свойства и методы нового класса.
Далее для установки границ цикла мы воспользовались свойством Size у TJSONArray, которое возвращает нам количество элементов массива.
Так как каждый элемент нашего массива — это объект, то в цикле мы приводим каждый элемент массива к типу TJSONObject.
Далее, для примера, я вывел для каждого элемента массива значение title — заголовка задачи.
Опять же обращу внимание на то, что использовал выбор значения пары по имени только потому, что 100% знал, что такая пара в объекте присутствует, если бы я разбирал объект с заранее неизвестным составом пар, то воспользовался бы выборкой по индексу.
Теперь посмотрим как используется перечислитель TJSONArrayEnumerator и закончим наш пример разбора JSON-объекта с задачами:
procedure Tfmain.EnumArray2;
var JsonArray: TJSONArray;
Enum: TJSONArrayEnumerator;
Task : TTaskResource;
begin
{получаем масиив из значения пары с названием "items"}
JsonArray:=FJSONObject.Get('items').JsonValue as TJSONArray;
{создаем перечислитель}
Enum:=TJSONArrayEnumerator.Create(JsonArray);
{перебираем все элементы массива}
while Enum.MoveNext do
begin
Task:=TTaskResource.Create;
{передали значение в метод для парсинга}
Task.ParseJSON(Enum.GetCurrent);
Task.Free;
end;
end;
В этом примере мы создали перечислитель типа TJSONArrayEnumerator, передав в качестве параметра конструктора полученный из JSON-объекта массив, а затем, воспользовавшись в цикле While методом MoveNext перечислили все элементы массива, передав их в метод для заполнения свойств класса, который мы рассматривали выше.
Для получения текущего элемента массива можно использовать либо метод GetCurrent (как это делается в примере выше), либо использовать свойство объекта Current.
На этом про чтение данных из JSON-объекта можно закончить и перейти к следующей части — записи данных в формате JSON.
Запись данных
Рассмотрим абстрактный пример, в котором запишем в JSON-объект все возможные типы данных (строки, числа и т.д.).
procedure Tfmain.Button2Click(Sender: TObject);
var JSONObject, InnerObject : TJSONObject;
Pair : TJSONPair;
JsonArray: TJSONArray;
S:TStringList;
begin
S:=TStringList.Create;
{создали объект верхнего уровня}
try
JSONObject:=TJSONObject.Create;
{создали пару}
Pair:=TJSONPair.Create('stringPair','TestString');
{записали пару в объект}
JSONObject.AddPair(Pair);
{можно записать и так}
JSONObject.AddPair(TJSONPair.Create('stringPair2','TestString2'));
{и так}
JSONObject.AddPair(TJSONPair.Create('NumberPair2',TJSONNumber.Create(128)));
{добавляем в объект ещё один объект}
InnerObject:=TJSONObject.Create;
InnerObject.AddPair('Inner_StringPair','Hello World!');
JSONObject.AddPair(TJSONPair.Create('InnerObject',InnerObject));
{записываем в объект массив}
//создали пустой массив
JsonArray:=TJSONArray.Create();
JsonArray.Add('StringElement');//первый элемент - строка
JsonArray.Add(True); //второй элемент - True
JsonArray.Add(100500); //третий элемент - число
//четвертый элемент - объект
JsonArray.AddElement(TJSONObject.Create(TJSONPair.Create('PairName','PairValue')));
//записали массив в объект
JSONObject.AddPair('Array',JsonArray);
//сохраняем объект в файл
S.Add(JSONObject.ToString);
S.SaveToFile('json.txt');
finally
S.Free;
JSONObject.Destroy;
end;
end;
В представленном выше примере я постарался привести все возможные варианты записи JSON-объекта, а именно:
Запись строки
Запись числа
Запись булевых значений
Запись массива в котором каждый элемент содержит различные типы данных — строку, True, число, json-объект
Как можно видеть по примеры, вариантов записи данных масса, на в основе всей записи находится объект (TJSONObject) у которого есть несколько перегруженных методов AddPair для записи пар:
function AddPair(const Pair: TJSONPair): TJSONObject; overload;
function AddPair(const Str: TJSONString; const Val: TJSONValue): TJSONObject; overload;
function AddPair(const Str: UnicodeString; const Val: TJSONValue): TJSONObject; overload;
function AddPair(const Str: UnicodeString; const Val: UnicodeString): TJSONObject; overload;
Как нетрудно догадаться, т.к. AddPair возвращает нам TJSONObject, то запись объекта можно было бы осуществить и так в одну строку:
JSONObject.AddPair(Pair).AddPair(TJSONPair.Create('stringPair2','TestString2'));
Для удаление пар из объекта также существует метод:
function RemovePair(const PairName: String): TJSONPair;
В целом по записи можно сказать, что модуль DBXJSON.pas позволяет проводить запись JSON-объектов любой сложности (хотя, кто бы в этом сомневался). В итоге выполнения представленного выше метода в файл был записан вот такой объект:
{"stringPair":"TestString",
"stringPair2":"TestString2",
"NumberPair2":128,
"InnerObject":{"Inner_StringPair":"Hello World!"},
"Array":["StringElement",
true,
100500,
{"PairName":"PairValue"}
]
}
Вот, в принципе, все, что можно для начала рассказать про работу с JSON в Delphi.