Работа с JSON в Delphi 2010 — XE2



Впервые о том, что 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.
Теги:
json, dbxjson.pas, json delphi, delphi json, использование json в delphi, парсинг json в delphi, dbxjson.pas, delphi, delphi в web, Основы delphi
Добавлено: 24 Февраля 2015 20:11:28 Добавил: Андрей Ковальчук Нравится 0
Добавить
Комментарии:
Нету комментариев для вывода...