Про работу с 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 для доступа к значениям пар мы можем использовать:
- Перечслитель пар
- Доступ по пути к паре
- Доступ к значению пары по её имени
- и т.д.
Рассмотрим основные способы чтения значений пар из 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 заключается в следующем:
- пара intraday является вложенным объектом;
- каждая пара внутри 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 я провел следующие тесты:
- время создания нового JSON, содержащий пары без массивов и вложенных объектов
- время создания нового JSON с массивами и вложенными объектами
- время чтения JSON, содержащего пары без массивов и вложенных объектов
- время чтения 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 вполне подходит: простая, компактная, понятная даже начинающему пользователю библиотека.
Возможно, на скорость работы повлияет замена дефолтного менеджера памяти. Например, на этот: https://github.com/synopse/mORMot2/blob/master/src/core/mormot.core.fpcx64mm.pas
Да, Алексей,когда только начинал с Lazarus работать читал про менеджеры памяти для Lazarus но более подходящего, чем есть, к сожалению, не нашел. Спасибо за ссылку, посмотрю и отпишусь о результатах
И на счёт первого теста, там все пары с одним именем.
Vlad забыл поделить время на 10?
Провёл собственное расследование насчёт производительности парсинга :)
https://forum.lazarus.freepascal.org/index.php/topic,44140.msg389101.html#msg389101
Вероятно, есть небольшие расхождения в статье. По факту, константа AsJSONFormat должна форматировать только массивы и объекты внутри всего JSON-объекта, но она форматирует весь объект, что у Вас и продемонстрировано. Весь JSON-объект выводится сплошной строкой, хотя, судя по тому, какие опции заданы внутри константы, это не должно быть так. И возникает вопрос, в чем проблема?) Это косяк fpjson? Или так и должно быть? (хотя, также, судя по DefaultFormat=[], когда нет никаких опций — всё выводится нормально, и добавление foSingleLineArray должен работать только на массивы).
А так, спасибо за статью, очень много полезных вещей описано и разжёвано!