уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

Про работу с JSON в Delphi и писал в блоге и не один раз. При этом, последние две статьи касались работы с JSON в Delphi 10.3 Rio. Что же касается работы с json в Lazarus и Free Pascal, то такой информации до сегодняшнего момента в блоге webdelphi.ru не было. Попробуем исправить ситуацию и посмотреть как устроена работа с Json в lazarus.

Надо сказать, что для тех, кто знаком с json в delphi, перейти на работу с json в lazarus не составит никаких проблем. Однако, чтобы быть более менее последовательным в изложении, я буду выстраивать статью в том же порядке, в котором рассказывал о работе с json в delphi 10.3.

Введение

В Lazarus и Free Pascal (FPC) на данный момент имеется одна библиотека для работы с JSON, которая так и называется — fp-json. Все исходники библиотеки можно посмотреть в следующей директории:

$LAZARUS$\fpc\3.0.4\source\packages\fcl-json\src\

На момент написания этой статьи я пользуюсь Lazarus 2.0.8 с FPC 3.0.4 и здесь fp-json состоит из следующих модулей:

  • fpjson.pp
  • fpjsonrtti.pp
  • fpjsontopas.pp
  • jsonconf.pp
  • jsonparser.pp
  • jsonscanner.pp

Так как сегодня мы только знакомимся с устройством fp-json, то для знакомства нам будет достаточно использовать всего два модуля — это fpjson и jsonparser. С остальными модулями библиотеки будем знакомиться по мере необходимости.

Если вы хотите оценить сразу все возможности работы с json в lazarus, то для этого можно воспользоваться специальным проектом, который имеется в поставке Lazarus и находится в директории:

$LAZARUS$\tools\jsonviewer\

это приложение просмотрщик json, наподобие того, который я когда-то писал в Delphi, но с бОльшими возможностями. Выглядит приложение вот так:

Для дальнейшей работы, можете скомпилировать jsonViewer в Lazarus или же воспользоваться моим, скачать который можно по ссылке в конце вот этой статьи. Я в дальнейшем буду пользоваться своим просмотрщиком json.

JSON в Lazarus и Free Pascal

Как и полагается, fp-json поддерживает все типы данных JSON,которые в модуле fpjson представлены следующими классами:

  • TJSONNumber — число
    • TJSONFloatNumber — число с плавающей точкой (Double)
    • TJSONIntegerNumber — целочисленные значения (integer)
    • TJSONInt64Number — целочисленные значения (int64)
    • TJSONQWordNumber — целочисленные значения (QWord)
  • TJSONString — строка
  • TJSONBoolean — логический тип (boolean)
  • TJSONNull — неопределенное/пустое значение (Null)
  • TJSONArray — массив
  • TJSONObject — объект

Все эти классы являются потомками класса TJSONData. Также, в модуле fpjson определены следующие типы данных, касающиеся json:

простые типы данных

  TJSONFloat = Double;
  TJSONStringType = UTF8String;
  TJSONUnicodeStringType = Unicodestring;
  TJSONCharType = AnsiChar;
  PJSONCharType = ^TJSONCharType;

перечислители

TJSONtype = (jtUnknown, jtNumber, jtString, jtBoolean, jtNull, jtArray, jtObject);
TJSONInstanceType = (jitUnknown, jitNumberInteger,jitNumberInt64,jitNumberQWord,jitNumberFloat,
                       jitString, jitBoolean, jitNull, jitArray, jitObject);
TFormatOption = (foSingleLineArray, foSingleLineObject, foDoNotQuoteMembers, foUseTabchar, foSkipWhiteSpace);

Множества

TFormatOptions = set of TFormatOption;

Для демонстрации работы с json в lazarus начнем, как и в статье про delphi, с примеров создания json-объектов. Заодно, можно будет сравнить как это делается в Delphi, а как в Lazarus и FPC.

Создание JSON в Lazarus

Простые примеры JSON-объектов

Под словом «простой» я понимаю JSON-объект без вложенных массивов, других объектов и так далее, то есть объект, состоящий исключительно из пар «имя-значения», где значением выступают простые типы данных (строки, числа, true/false и null). В Lazarus такие объекты можно создавать двумя способами.

Первый способ — уже привычный нам: создаем объект Json и последовательно добавляем в него пары. В Lazarus это делается следующим образом:

var json: TJSONObject;
begin
  json:=TJSONObject.Create; //создали объект
  try
    {добавляем пары}
    json.Add('Hello', 'World');
    json.Add('IntNumber', 1);
    json.Add('FloatNumber', 5.45);
    json.Add('Boolean', True);
    json.Add('Null', TJSONNull.Create);
    //выводим объект в Memo
    Memo1.Lines.Add(json.AsJSON);
  finally
    FreeAndNil(json) //не забываем освободить память
  end;
end;

В результате мы получим вот такой json-объект:

"Hello":"World",
"IntNumber":1,
"FloatNumber":5.4500000000000002E+000,
"Boolean":true,
"Null":null
}

Который в jsonViewer будет выглядеть вот так:

В этом примере мы использовали для добавления пар метод Add, который имеет несколько перегруженных версий:

function Add(const AName: TJSONStringType; AValue: TJSONData): Integer; overload;
function Add(const AName: TJSONStringType; AValue: Boolean): Integer; overload;
function Add(const AName: TJSONStringType; AValue: TJSONFloat): Integer; overload;
function Add(const AName, AValue: TJSONStringType): Integer; overload;
function Add(const AName : String; AValue: TJSONUnicodeStringType): Integer; overload;
function Add(const AName: TJSONStringType; Avalue: Integer): Integer; overload;
function Add(const AName: TJSONStringType; Avalue: Int64): Integer; overload;
function Add(const AName: TJSONStringType; Avalue: QWord): Integer; overload;
function Add(const AName: TJSONStringType): Integer; overload;
function Add(const AName: TJSONStringType; AValue : TJSONArray): Integer; overload;

В отличие от Delphi, где используемый метод AddPair возвращает нам ссылку на объект, метод Add в Lazarus возвращает индекс новой пары в списке.

Второй способ. Передать в конструктор массив констант. В этом случае нам нет необходимости использовать метод Add — его использует конструктор. Например, создадим тот же самый объект, что и в предыдущем примере:

var json: TJSONObject;
begin
  //создали объект
  json:=TJSONObject.Create(['Hello','World',
                            'IntNumber',1,
                            'FloatNumber', 5.45,
                            'Boolean', True,
                            'Null', TJSONNull.Create]);
  try
    //выводим объект в Memo
    Memo1.Lines.Add(json.AsJSON);
  finally
    FreeAndNil(json) //не забываем освободить память
  end;
end;

Здесь чётные элементы массива — это название пары, а нечетные — значения. В результате выполнения представленного кода мы получим абсолютно тот же самый json-объект, что и представленный выше.

Единственный момент, который необходимо учитывать при использовании этого способа создания json в lazarus — это то, что количество элементов массива должно быть всегда чётным (в нашем случае элементов 10), иначе мы получим вот такое исключение:

TJSONObject must be constructed with name,value pairs

В целом, оба варианта хороши и могут использоваться в зависимости от ситуации, а уж какой вариант будет по душе вам — выбирайте сами.

Создание массивов и вложенных объектов JSON в Lazarus и FPC

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

{
  "kind":"tasks#task",
  "title":"Новая задача",
  "links":
  [
        {
      "type":"href",
      "description":"desc",
      "link":"http://webdelphi.ru"
    }
,
    
    {
      "type":"href2",
      "description":"desc",
      "link":"http://webdelphi.ru"
    }
  ]
}

Создадим его, используя Lazarus и FPC. Наш «сложный» json-объект состоит из трех пар, где третья пара (links) — это массив с двумя элементами, и каждый элемент массива — это объект на три пары. Словесное описание этого json-объекта выглядит сложнее, чем его создание :) Вот как можно создать этот json, используя знания о создании json:

var json: TJSONObject;
    jsonArray: TJSONArray;
begin
  //создали объект с первыми двумя парами
  json:=TJSONObject.Create(['kind','tasks#task',
                            'title','Новая задача']);
  try
    //создаем массив
    jsonArray:=TJSONArray.Create;
  //добавляем в массив два объекта с тремя парами в каждом
  //при создании объекта сразу добавляем в него необходимые пары
    jsonArray.Add(TJSONObject.Create(['type','href',
                                    'description','desc',
                                    'link','http://webdelphi.ru']));
 
    jsonArray.Add(TJSONObject.Create(['type','href2',
                                    'description','desc',
                                    'link','http://webdelphi.ru']));
  //добавляем массив в родительский объект
    json.Add('links',jsonArray);
    //выводим объект в Memo
    Memo1.Lines.Add(json.AsJSON);
  finally
    FreeAndNil(json) //не забываем освободить память
  end;
end;

Как видите, использование перегруженного конструктора у TJSONObject позволяет нам с минимальными затратами времени создать json с вложенными массивами и объектами. Кстати, у TJSONArray также есть перегруженный конструктор, как и у TJsonObject, что позволяет создавать массивы с простыми элементами в одну строку кода, не прибегая к использованию методов Add, которые в TJsonArray имеют то же описание, что и у TJsonObject за исключением одного:

function Add(AnObject: TJSONObject): Integer;

Преобразование JSON в String

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

function GetAsBoolean: Boolean; virtual; abstract;
function GetAsFloat: TJSONFloat; virtual; abstract;
function GetAsInteger: Integer; virtual; abstract;
function GetAsInt64: Int64; virtual; abstract;
function GetAsQWord: QWord; virtual; abstract; 
function GetAsJSON: TJSONStringType; virtual; abstract;
function GetAsString: TJSONStringType; virtual; abstract;

В зависимости от того, в объекте какого класса мы вызываем перегруженные версии этих методов мы можем получить в результате либо исключение, либо требуемое значение (строку, число и т.д.). Что касается преобразования json-объектов в строки, то здесь мы можем воспользоваться методом GetAsJson, который вызывается при использования свойства объекта:

Property AsJSON : TJSONStringType Read GetAsJSON;

В принципе, в коде выше именно это свойство я использовал. По умолчанию, Lazarus преобразует JSON в одну строку. Например, JSON-объект, который мы создали в предыдущем примере, по умолчанию преобразуется вот в такую строку:

{ "kind" : "tasks#task", "title" : "Новая задача", "links" : [{ "type" : "href", "description" : "desc", "link" : "http://webdelphi.ru" }, { "type" : "href2", "description" : "desc", "link" : "http://webdelphi.ru" }] }

Метод AsJson в Lazarus аналогичен методу ToString при работе с JSON в Delphi. Вместе с этим, иногда требуется представить JSON-объект в более читабельном виде — c отступами, переносами и так далее. Для этого в FPC и Lazarus используется метод:

FormatJSON(Options : TFormatOptions = DefaultFormat; Indentsize : Integer = DefaultIndentSize) : TJSONStringType;

Options — это множество, содержащее опции форматирования JSON. В качестве опций форматирования JSON можно использовать следующие значения:

TFormatOption = (foSingleLineArray,   //Массивы будут представлены одной строкой без переносов элементов
                   foSingleLineObject,  //Объекты будут представлены одной строкой без переносов элементов 
                   foDoNotQuoteMembers, //Не заключать в кавычки имена пар
                   foUseTabchar,        //Использоваться табуляцию для отступов вместо пробелов
                   foSkipWhiteSpace);   //не использовать whitespace при форматировании JSON

Indentsize — отступы в JSON (по умолчанию Indentsize=2).
Разработчики fp-json постарались предусмотреть различные варианты форматирования JSON и определили в модуле fpjson следующие константы, которые можно использовать в качестве первого параметра функции FormatJSON:

  DefaultFormat     = [];
  AsJSONFormat      = [foSingleLineArray,foSingleLineObject]; 
  AsCompressedJSON  = [foSingleLineArray,foSingleLineObject,foskipWhiteSpace]; 
  AsCompactJSON     = [foSingleLineArray,foSingleLineObject,foskipWhiteSpace,foDoNotQuoteMembers];

Ниже я продемонстрировал то, как будет выглядеть строка с JSON при использовании каждой из констант, представленных выше:

DefaultFormat

{
  "kind" : "tasks#task",
  "title" : "Новая задача",
  "links" : [
    {
      "type" : "href",
      "description" : "desc",
      "link" : "http://webdelphi.ru"
    },
    {
      "type" : "href2",
      "description" : "desc",
      "link" : "http://webdelphi.ru"
    }
  ]
}

AsJSONFormat

{ "kind" : "tasks#task", "title" : "Новая задача", "links" : [{ "type" : "href", "description" : "desc", "link" : "http://webdelphi.ru" }, { "type" : "href2", "description" : "desc", "link" : "http://webdelphi.ru" }] }

AsCompressedJSON

{"kind":"tasks#task","title":"Новая задача","links":[{"type":"href","description":"desc","link":"http://webdelphi.ru"},{"type":"href2","description":"desc","link":"http://webdelphi.ru"}]}

AsCompactJSON

{kind:"tasks#task",title:"Новая задача",links:[{type:"href",description:"desc",link:"http://webdelphi.ru"},{type:"href2",description:"desc",link:"http://webdelphi.ru"}]}

Парсинг JSON в Lazarus и FPC

Для разбора (парсинга) JSON в Lazarus/Free Pascal необходимо подключить в uses модули jsonparser и jsonscanner которые содержат классы и типы данных для парсинга JSON. В модуле jsonparser содержится всего один класс:

TJSONParser = Class(TObject)

Парсер возвращает объект типа TJsonData, который мы, в дальнейшем, можем использовать для чтения необходимых нам значений из JSON. Для начала работы нам необходимо создать объект TJSONParser, используя один из конструкторов:

constructor Create(Source: TStream; AOptions: TJSONOptions); overload;
constructor Create(const Source: String; AOptions: TJSONOptions); overload;

а, затем, вызвать метод

function Parse: TJSONData;

который и производит разбор JSON.
Попробуем разобрать JSON-объект, который я использовал в качестве примера в статье про JSON в Delphi 10.3. Вот как он выглядел:

{
  "firstName":"Петров",
  "lastName":"Пётр",
  "address":
  {
    "streetAddress":"ул. Дельфийская, 101, кв.101",
    "city":"Дельфийск",
    "postalCode":"100301"
  }
,
  "phoneNumbers":
  [
    "812 123-1234",
    "916 123-4567"
  ]
}

Минимальный код для парсинга JSON в Lazarus может выглядеть вот так:

var JsonParser: TJSONParser;
    JsonData: TJSONData;
begin
  JsonParser:=TJSONParser.Create(cJsonStr,DefaultOptions);
  try
    JsonData:=JsonParser.Parse;
  finally
    FreeAndNil(JsonData);
    FreeAndNil(JsonParser);
  end;

Здесь мы создали JsonParser и, используя метод Parse, получили объект JsonData для дальнейшей работы с парами Json в Lazarus. Теперь рассмотрим несколько примеров того, как можно получить значения пар из JSON.

Пример №1. Работа с парами JSON в Lazarus и FPC

Как и в случае с Delphi, в Lazarus и Free Pascal для доступа к значениям пар мы можем использовать:

  1. Перечслитель пар
  2. Доступ по пути к паре
  3. Доступ к значению пары по её имени
  4. и т.д.

Рассмотрим основные способы чтения значений пар из JSON в Lazarus/FPC.

1.1. Доступ к значению пары по её имени

Прочитаем из нашего JSON-объекта имя и фамилию человека, используя имена пар:

var JsonParser: TJSONParser;
    JsonObject: TJSONObject;
begin
  JsonParser:=TJSONParser.Create(cJsonStr,DefaultOptions);
  try
    JsonObject:=JsonParser.Parse as TJSONObject;
    Memo1.Lines.Add(JsonObject.Strings['firstName']);
    Memo1.Lines.Add(JsonObject.Strings['lastName']);
  finally
    FreeAndNil(JsonObject);
    FreeAndNil(JsonParser);
  end;
end;

1.2. Доступ к значению пары по её имени с использованием значения по умолчанию

Этот вариант может подойти в случаях, когда не известно есть ли пара с заданным именем в JSON и, если такой пары нет, то присвоить переменной значение по умолчанию. Сделать это можно, используя один из перегруженных методов Get. Например, при чтении имени и фамилии человека, метод Get можно использовать вот так:

var JsonParser: TJSONParser;
    JsonObject: TJSONObject;
begin
  JsonParser:=TJSONParser.Create(cJsonStr,DefaultOptions);
  try
    JsonObject:=JsonParser.Parse as TJSONObject;
    Memo1.Lines.Add(JsonObject.Get('firstName', 'Неизвестная фамилия'));
    Memo1.Lines.Add(JsonObject.Get('lastName', 'Неизвестное имя'));
  finally
    FreeAndNil(JsonObject);
    FreeAndNil(JsonParser);
  end;
end;

В fp-utils имеются следующие методы Get:

    Function Get(Const AName : String) : Variant;
    Function Get(Const AName : String; ADefault : TJSONFloat) : TJSONFloat;
    Function Get(Const AName : String; ADefault : Integer) : Integer;
    Function Get(Const AName : String; ADefault : Int64) : Int64;
    Function Get(Const AName : String; ADefault : QWord) : QWord;
    Function Get(Const AName : String; ADefault : Boolean) : Boolean;
    Function Get(Const AName : String; ADefault : TJSONStringType) : TJSONStringType;
    Function Get(Const AName : String; ADefault : TJSONUnicodeStringType) : TJSONUnicodeStringType;
    Function Get(Const AName : String; ADefault : TJSONArray) : TJSONArray;
    Function Get(Const AName : String; ADefault : TJSONObject) : TJSONObject;

Здесь же, стоит упомянуть и о поиске пары по её имени. Для этого можно использовать методы Find:

    Function Find(Const AName : String) : TJSONData; overload;
    Function Find(Const AName : String; AType : TJSONType) : TJSONData; overload;
    function Find(const key: TJSONStringType; out AValue: TJSONData): boolean;
    function Find(const key: TJSONStringType; out AValue: TJSONObject): boolean;
    function Find(const key: TJSONStringType; out AValue: TJSONArray): boolean;
    function Find(const key: TJSONStringType; out AValue: TJSONString): boolean;
    function Find(const key: TJSONStringType; out AValue: TJSONBoolean): boolean;
    function Find(const key: TJSONStringType; out AValue: TJSONNumber): boolean;

В отличие от Get, Find возвращает во втором параметре саму пару, а в качестве результата — True/False, в зависимости от того найдена пара с заданным именем или нет.

1.3. Доступ к значению пары по её пути

Например, нам необходимо получить название улицы из нашего JSON-объекта. Мы бы могли использовать несколько раз Find/Get и добраться до пары streetAddress, а можем сделать наш код намного короче, используя путь к паре вот так:

JsonObject.FindPath('address.streetAddress').AsString

Путь к паре указывается через точку. То есть я «попросил» у объекта JsonObject вернуть пару streetAddress, которая находится во вложенном объекте address. Метод FindPath возвращает объект типа TJsonData, поэтому для получения строки адреса я использовал также свойство TJsonData.AsString.

1.4. Доступ к парам с использованием перечислителя

Четвертый способ получения пар из JSON — использование перечислителя. Этот способ я обычно использую в тех случаях, когда точно знаю, что из полученного JSON-объекта мне потребуется большинство пар или вообще все пары. Например, перечислим все пары со строковыми значениями:

var
  JsonParser: TJSONParser;
  JsonObject: TJSONObject;
  JsonEnum: TBaseJSONEnumerator;//перечислитель пар
begin
  JsonParser := TJSONParser.Create(cJsonStr, DefaultOptions);
  try
    JsonObject := JsonParser.Parse as TJSONObject;
    try
      JsonEnum := JsonObject.GetEnumerator;//получаем перечислитель пар
      try
        //проходим по каждой паре в объекте
        while JsonEnum.MoveNext do
          //проверяем, является ли значение пары строкой
          if JsonObject.Types[JsonEnum.Current.Key] = jtString then
            Memo1.Lines.Add(JsonEnum.Current.Value.AsString);
      finally
        FreeAndNil(JsonEnum)
      end;
    finally
      FreeAndNil(JsonObject);
    end;
  finally
    FreeAndNil(JsonParser);
  end;
end;

Соответственно, зная тип пары, мы можем выстраивать логику нашего приложения по своему усмотрению — получать доступ к элементам массива, к вложенным объектам и так далее. В fp-json определены следующие типы пар:

TJSONtype = (jtUnknown, jtNumber, jtString, jtBoolean, jtNull, jtArray, jtObject);

Например, перечислим все телефоны из массива phoneNumbers нашего JSON-объекта

var
  JsonParser: TJSONParser;
  JsonObject: TJSONObject;
  JsonEnum: TBaseJSONEnumerator;//перечислитель пар
  i: integer;
begin
  JsonParser := TJSONParser.Create(cJsonStr, DefaultOptions);
  try
    JsonObject := JsonParser.Parse as TJSONObject;
    try
      JsonEnum := JsonObject.GetEnumerator;//получаем перечислитель пар
      try
        //проходим по каждой паре в объекте
        while JsonEnum.MoveNext do
          //проверяем, является ли значение пары массивом
          if JsonObject.Types[JsonEnum.Current.Key] = jtArray then
             //выводим значения каждого элемента в Memo
             for i:=0 to Pred(TJSONArray(JsonEnum.Current.Value).Count) do
               Memo1.Lines.Add(TJSONArray(JsonEnum.Current.Value).Items[i].AsString);
      finally
        FreeAndNil(JsonEnum)
      end;
    finally
      FreeAndNil(JsonObject);
    end;
  finally
    FreeAndNil(JsonParser);
  end;
end;

В этом примере я точно знаю, что каждый элемент массива — это строка, поэтому спокойно использовал свойство AsString для вывода значения элемента массива в Memo. Если вы не уверены в том, что все элементы массива имеют один и тот же тип, то лучше перестраховаться и перед выводом значения проверить тип пары. Ну и, раз уж затронули работу с массивами, то рассмотрим более подробно работу с массивами json в lazarus и fpc.

Пример №2. Работа с массивами JSON в Lazarus и FPC

Для работы с json-массивами в Lazarus и Free Pascal предусмотрен класс TJsonArray (наследник TJsonData) который содержит необходимые свойства и методы для манипулирования данными в массиве. Выше в примере я использовал свойства Count (количество элементов в массиве) и Items для доступа к каждому элементу. Однако, никто нам не запрещает использовать при работе тот же самый перечислитель TBaseJSONEnumerator для последовательного прохождения по каждому элементу json-массива. Работа с перечислителем массива json ничем не отличается от работы с перечислителем объекта, как мы делали это выше. Вот, например, как можно перечислить элементы массива с телефонами:

var
  JsonParser: TJSONParser;
  JsonObject: TJSONObject;
  JsonEnum, JsonArrayEnum: TBaseJSONEnumerator;//перечислитель пар
begin
  JsonParser := TJSONParser.Create(cJsonStr, DefaultOptions);
  try
    JsonObject := JsonParser.Parse as TJSONObject;
    try
      JsonEnum := JsonObject.GetEnumerator;//получаем перечислитель пар
      try
        //проходим по каждой паре в объекте
        while JsonEnum.MoveNext do
          //проверяем, является ли значение пары массивом
          if JsonObject.Types[JsonEnum.Current.Key] = jtArray then
             begin
               //получаем перечислитель массива
               JsonArrayEnum:=TJSONArray(JsonEnum.Current.Key).GetEnumerator;
               try
                 //перечисляем все эдементы
                 while JsonArrayEnum.MoveNext do
                   Memo1.Lines.Add(JsonArrayEnum.Current.Value.AsString);
               finally
                 FreeAndNil(JsonArrayEnum)
               end;
             end;
      finally
        FreeAndNil(JsonEnum)
      end;
    finally
      FreeAndNil(JsonObject);
    end;
  finally
    FreeAndNil(JsonParser);
  end;
end;

Пример №3. Работа с вложенными объектами json в lazarus и fpc

Почему-то именно эта часть работы с json всегда вызывала больше всего вопросов у читателей блога webdelphi.ru. На самом деле, зная структуру json и основы манипулирования различными данными json в lazarus и free pascal, работа с json любой сложности не составит никакого труда. Например, рассмотрим вот такой json-объект:

{
  "symbol":"AAPL",
  "stock_exchange_short":"NASDAQ",
  "timezone_name":"America/New_York",
  "intraday":
  {
    "2018-10-1915:59:00":
    {
      "open":"23",
      "low":"4"
    }
,
    "2018-10-1915:58:00":
    {
      "open":"25",
      "low":"21"
    }
  }
}

Основная сложность при разборе такого json заключается в следующем:

  1. пара intraday является вложенным объектом;
  2. каждая пара внутри intraday — это также вложенный объект и, при этом, имя пары может меняться, так как имя пары — это дата.

Разобрать такой вложенный json-объект можно достаточно просто, используя перечислитель TBaseJSONEnumerator. Вот, например, как можно вывести все значения и даты из intraday:

var
  JsonParser: TJSONParser;
  JsonObject, JsonNestedObj: TJSONObject;
  JsonEnum: TBaseJSONEnumerator;//перечислитель пар
begin
  JsonParser := TJSONParser.Create(cJsonStr, DefaultOptions);
  try
    //получили Json-объект
    JsonObject := JsonParser.Parse as TJSONObject;
    try
      //получили доступ к вложенному объекту intraday
      JsonNestedObj:=JsonObject.FindPath('intraday') as TJSONObject;
      //получаем перечислитель пар вложенного объекта
      JsonEnum := JsonNestedObj.GetEnumerator;
      try
        //проходим по каждой паре в объекте intraday
        while JsonEnum.MoveNext do
           begin
             //выводим имя пары (дату) в Memo
             Memo1.Lines.Add(JsonEnum.Current.Key);
             //последовательно выводим значение пар open и low
             Memo1.Lines.Add(TJSONObject(JsonEnum.Current.Value).Integers['open'].ToString);
             Memo1.Lines.Add(TJSONObject(JsonEnum.Current.Value).Integers['low'].ToString);
           end;
      finally
        FreeAndNil(JsonEnum)
      end;
    finally
      FreeAndNil(JsonObject);
    end;
  finally
    FreeAndNil(JsonParser);
  end;
end;

Здесь я воспользовался исключительно теми способами разбора json в lazarus, которые рассматривал выше — получил доступ к вложенному объекту json и, используя перечислтель пар, прошелся по каждой паре вложенного json-объекта.

Жизненный цикл объектов в fp-json

В fp-json родительский объект владеет любым из содержащихся в нем значений. Поэтому, когда я рассматривал вопросы создания json-объектов, то в конце метода уничтожал только родительский объект, не затрагивая при этом, уничтожение массивов, вложенных объектов и т.д. В отличие от Delphi, элементы json не имеют свойства Owned.

Производительность fp-json

Как и в случае с Delphi, чтобы оценить производительность fp-json я провел следующие тесты:

  1. время создания нового JSON, содержащий пары без массивов и вложенных объектов
  2. время создания нового JSON с массивами и вложенными объектами
  3. время чтения JSON, содержащего пары без массивов и вложенных объектов
  4. время чтения JSON, содержащего пары с массивами и вложенными объектами.

На чем тестировалась работа:

  • Процессор Intel Core i5 8400 (6-ти ядерный)
  • ОЗУ: 16 ГБ
  • ОС: Windows 10 x64

Производительность fp-json при создании JSON

В качестве первого теста я создавал вот такой JSON:

{
  "Pair":"Value",
  "Pair":"Value",
  ...
}

Количество пар в json-объекте: 500 000. Код для тестирования получился следующий:

var
  JSON: TJSONObject;
  PName, PValue: string;
  Count, Limit: integer;
  i,j:integer;
begin
  Count :=10;    //количество повторений теста
  Limit :=500000;//количество пар в json
  PName :='Pair';
  PValue:='Value';
 
  for I := 1 to Count do
  begin
    JSON := TJSONObject.Create;
    try
      for J := 0 to Pred(Limit) do
        JSON.Add(PName, PValue);
    finally
      FreeAndNil(JSON);
    end;
  end;
end;

Время выполнения операции засекалось при помощи компонента TEpikTimer, о котором я, возможно, расскажу в одной из следующих статей про Lazarus и Free Pascal. А пока посмотрим на результаты теста.

Результаты тестирования:

  • Размер полученного JSON: 7500 kb
  • Максимальное время: 1582 мс.
  • Минимальное время: 1552 мс.
  • Среднее время: 1561 мс.

По сравнению с Delphi, родная библиотека Lazarus, к сожалению, показала достаточно слабые результаты. На аналогичном тесте «JSON Objects Framework» от Embarcadero показывала результаты 156 и 144 мс. соответственно.

Для второго теста я создавал JSON следующего содержания:

{
  "Array":
  [
    "TestValue",
    "TestValue"
  ]
,
  "Array":
  [
    "TestValue",
    "TestValue"
  ]
}

Количество массивов в JSON: 10
Количество элементов массива: 500 000

Код теста:

var
  JSON: TJSONObject;
  JSONArray: TJSONArray;
  ArrayCount, ArrayLimit: integer;
  TestCount: integer;
  i,j,k: integer;
begin
  ArrayCount :=  10;
  ArrayLimit := 500000;
  TestCount:=10;
 
  for I := 1 to TestCount do
  begin
    JSON := TJSONObject.Create;
    try
      for j := 1 to ArrayCount do
        begin
          JSONArray:=TJSONArray.Create;
          for k := 1 to ArrayLimit do
            JSONArray.Add('TestValue');
          JSON.Add('Array',JSONArray);
        end;
    finally
      FreeAndNil(JSON);
    end;
  end;
end;

Результаты тестирования:

  • Размер полученного JSON: 60 Mb
  • Максимальное время: 10900 мс.
  • Минимальное время: 10653 мс.
  • Среднее время: 10829 мс

С результатами аналогичного теста в Delphi можно ознакомиться здесь.

Производительность fp-json при парсинге JSON

Разбор JSON, содержащего пары значений:

  • Количество пар в JSON: 500 000
  • Размер файла, содержащего JSON: 7500 kb

Код теста:

var
  S: TFileStream;
  JSON: TJSONParser;
  JSONData: TJSONData;
  i: integer;
begin
  //считываем файл в TFileStream
  S := TFileStream.Create('json.txt', fmOpenRead);
  try
    for I := 1 to 10 do
    begin
      //засекаем время от момента создания TJSONParser
      Json:=TJSONParser.Create(S, DefaultOptions);
      try
         JSONData:=JSON.Parse;
      finally
        FreeAndNil(JSONData);
        FreeAndNil(JSON);
      end;
      //до момента уничтожения объектов 
      S.Position:=0;
    end;
  finally
    FreeAndNil(S);
  end;
end;

Результаты тестирования:

  • Максимальное время: 3239 мс.
  • Минимальное время: 3100 мс.
  • Среднее время: 3159 мс

Разбор JSON, содержащего массивы на 500 000 элементов показал следующий результат:

  • Максимальное время: 21895 мс.
  • Минимальное время: 21429 мс.
  • Среднее время: 21632 мс

С результатами аналогичной работы в Delphi можно ознакомиться здесь.

Выводы

Итак, в этой статье я постарался рассмотреть базовые возможности работы с json в lazarus и free pascal с использованием родной библиотеки fp-json. По своим возможностям эта библиотека напоминает «JSON Objects Framework» в Delphi, хотя и имеет свои достаточно полезные и интересные фичи. Не могу сказать, что я на 100% доволен этой библиотекой lazarus, хотя бы даже по тестированию её производительности. Возможно, что есть какие-то способы ускорить её работу, но я рассматривал работу со стороны начинающего пользователя и не гарантирую, что те способы парсинга json в lazarus, которые я показал являются самыми оптимальными. Однако, для небольших проектов, использующих в работе json, думаю, что fp-json вполне подходит: простая, компактная, понятная даже начинающему пользователю библиотека.

уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.
4.9 12 голоса
Рейтинг статьи
Подписаться
Уведомить о
8 Комментарий
Межтекстовые Отзывы
Посмотреть все комментарии
Kazantsev Alexey
Kazantsev Alexey
16/05/2020 23:14

Возможно, на скорость работы повлияет замена дефолтного менеджера памяти. Например, на этот: https://github.com/synopse/mORMot2/blob/master/src/core/mormot.core.fpcx64mm.pas

Kazantsev Alexey
Kazantsev Alexey
16/05/2020 23:39

И на счёт первого теста, там все пары с одним именем.

avk
avk
16/12/2020 20:29

Vlad забыл поделить время на 10?

avk
avk
01/01/2021 15:37

Провёл собственное расследование насчёт производительности парсинга :)
https://forum.lazarus.freepascal.org/index.php/topic,44140.msg389101.html#msg389101

blackspaceghost
blackspaceghost
12/08/2021 14:41

Вероятно, есть небольшие расхождения в статье. По факту, константа AsJSONFormat должна форматировать только массивы и объекты внутри всего JSON-объекта, но она форматирует весь объект, что у Вас и продемонстрировано. Весь JSON-объект выводится сплошной строкой, хотя, судя по тому, какие опции заданы внутри константы, это не должно быть так. И возникает вопрос, в чем проблема?) Это косяк fpjson? Или так и должно быть? (хотя, также, судя по DefaultFormat=[], когда нет никаких опций — всё выводится нормально, и добавление foSingleLineArray должен работать только на массивы).

blackspaceghost
blackspaceghost
12/08/2021 14:42

А так, спасибо за статью, очень много полезных вещей описано и разжёвано!

Борис Боговедов
Борис Боговедов
31/03/2024 19:57

Друг, спасибо за статью!
Есть верный способ излечиться — это поверить Господу Богу, что ранами Его мы исцелились.
Проси Бога, верь и говори, что ты исцелён.