Пользовательский конвертер для TJsonMarshal в Delphi 11

Я пытаюсь зарегистрировать собственный конвертер, чтобы избежать унаследованных свойств в TObjectList - в основном FListHelper и FOwnsObjects. Но я не могу зарегистрировать собственный конвертер, а в документации нет реальных примеров.

(Нет, это не дубликат: Как скрыть свойства TObjectList «ownsObjects» и «listHelper» из Json с помощью Delphi (Rest.JSON)?)

Я пытаюсь зарегистрировать конвертер, но, похоже, он никогда не запускается.

Я обернул его в свой собственный класс, который выглядит так:

TMyJsonConverter = class(TJsonConverter)
  class function JsonConvert(ObjectToConvert:TObject): string;
  private
    type
      TListOfObjectInterceptor = class(TJSONInterceptor)
        function ObjectsConverter(Data: TObject; Field:string): TListOfObjects; override;
    end;
  end;


function TMyJsonConverter.TListOfObjectInterceptor.ObjectsConverter(Data: TObject; Field:string): TListOfObjects;
begin
  raise Exception.Create('converter found');
end;

class function TMyJsonConverter.JsonConvert(ObjectToConvert: TObject): string; 
begin
  var customConverter := TMyJsonConverter.TListOfObjectInterceptor.Create();
  var otherConverter := TMyJsonConverter.Create;
  var marshaller := TJSONMarshal.Create(otherconverter);

  marshaller.RegisterConverter(
    ObjectToConvert.ClassType,
    '*',
    customConverter.ObjectsConverter
  );

  var json := marshaller.Marshal(ObjectToConvert);

  try
    exit(json.ToString);
  finally
    marshaller.Free;
  end;
end;

Я пытался зарегистрировать типы TObjectConverter, TObjectsConverter, TTypeObjectsConverter, но, похоже, мне так и не удалось войти в функцию преобразования. Я вижу, что вызов регистрации действительно регистрирует преобразователь, но когда я маршалирую JSON, он снова не находит пользовательский преобразователь.

Вот пример структуры, освещающей проблему, которую я хочу маршалировать TMySampleDTO как JSON:

type
  TEmployee = class
  public
    Id: Integer;
    Name: string;
  end;
  TEmployeeList = class(TObjectList<TEmployee>);

  TWorktime = class
  public
    EmployeeId: Integer;
    DepartmentId: Integer;
    StartTime: TDateTime;
    StopTime: TDateTime;
  end;
  TWorktimeList = class(TObjectList<TWorktime>);

  TDepartment = class
  public
    Id: Integer;
    Address: string;
    Employees: TEmployeelist;
  end;
  TDepartmentList = class(TObjectList<TDepartment>);

  TMySampleDTO = class
  public
    Departments: TDepartmentList;
    Worktimes: TWorktimeList;
    Employees: TEmployeeList;
  end;

ОБНОВЛЕНИЕ: мне удалось запустить конвертер, хотя Embarcadero определил константу FIELD_ANY как '*', он не запустится, если вы не укажете точное имя поля, в моем случае FListHelper. Однако здесь возникает следующая проблема: мне также нужно указать точный тип, поскольку он не проверяет наследование. Итак, если моя структура объекта имеет свойства, производные от TObjectList<T>, все эти списки будут сериализованы как объекты со списком в качестве свойства.

Не могли бы вы предоставить пример структуры данных, которую вы пытаетесь сериализовать в JSON?

Peter Wolf 25.09.2023 09:52

@PeterWolf Я добавил некоторые данные, которые могут показать мою проблему :)

Matt Baech 25.09.2023 10:06

Отвечает ли это на ваш вопрос? Как скрыть свойства TObjectList «ownsObjects» и «listHelper» из Json с помощью Delphi (Rest.JSON)?

Peter Wolf 25.09.2023 10:22

К сожалению, нет, я внимательно прочитал этот ответ, и именно это натолкнуло меня на путь создания пользовательского конвертера. Моя проблема в том, что я хочу, чтобы это был общий конвертер для всех моих объектов. И я должен сказать, что считаю некрасивым аннотировать все мои модели атрибутами JsonReflect. По сути, я хочу сделать то, что сделал Стефан в своем ответе, но без атрибута.

Matt Baech 25.09.2023 10:27

@RemyLebeau Спасибо за очистку, однако я не ответил на вопрос, как уже упоминалось, теперь он называется, но все еще не работает должным образом.

Matt Baech 26.09.2023 07:58

@MattBaech См. функцию ListTypeConverter и ее регистрацию в моем ответе. Например, единая регистрация преобразователя типов для TEmployeeList будет применяться как к полю Employees класса TMySampleDTO, так и к полю Employees класса TDepartment.

Peter Wolf 26.09.2023 08:33
Как сделать HTTP-запрос в Javascript?
Как сделать HTTP-запрос в Javascript?
В JavaScript вы можете сделать HTTP-запрос, используя объект XMLHttpRequest или более новый API fetch. Вот пример для обоих методов:
0
6
89
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

В документации для TTypeMarshaller.RegisterConverter отсутствует какая-либо подробная информация, и у вас нет другого выбора, кроме как изучить исходный код Delphi RTL. На первый взгляд все перегруженные версии метода можно разделить на 3 группы:

  1. Перегрузка с 3 параметрами с последним параметром типа TConverterEvent — регистрирует преобразователь, который выполняет преобразование на основе своего свойства ConverterType . Это свойство доступно только для чтения, но его значение устанавливается вместе с заданием любого из его делегатов преобразования (*Converter свойств).
  2. Перегружает 3 параметра с последним параметром типа T...Converter — регистрирует преобразователь для FieldName (2-й параметр) внутри типа класса clazz (1-й параметр). Назовем его «преобразователем поля».
  3. Перегружает 2 параметра с последним параметром типа TType...Converter — регистрирует преобразователь для типа класса clazz (1-й параметр). По сути, это вызывает первую перегрузку, в которой для FieldName установлено значение '*', которое зарезервировано для преобразователей типов.

В примере кода вы регистрируете преобразователь полей для поля с именем * внутри типа класса экземпляра, предоставленного в качестве аргумента вашего метода TMyJsonConverter.JsonConvert. Это объясняет, почему ваша процедура преобразования не вызывается.

Кроме того, вы создаете экземпляр TListOfObjectInterceptor, чтобы передать ссылку на его метод ObjectsConverter методу TTypeMarshaller.RegisterConverter. Таким образом вы слили этот TListOfObjectInterceptor экземпляр. Перехватчики следует использовать в сочетании с атрибутом JsonReflect.

Правильный способ регистрации преобразователя полей:

function ListFieldConverter(Data: TObject; Field: string): TListOfObjects;
var
  RttiContext: TRttiContext;
  List: TList<TObject>;
begin
  List := TList<TObject>(RttiContext.GetType(Data.ClassInfo).GetField(Field).GetValue(Data).AsObject);
  Result := TListOfObjects(List.List);
  SetLength(Result, List.Count); // makes unique copy
end;

{ ... }

marshaller.RegisterConverter(TMySampleDTO, 'Employees', ListFieldConverter);
marshaller.RegisterConverter(TMySampleDTO, 'Departments', ListFieldConverter);
marshaller.RegisterConverter(TDepartment, 'Employees', ListFieldConverter);
marshaller.RegisterConverter(TMySampleDTO, 'Worktimes', ListFieldConverter);

Правильный способ регистрации преобразователя типов:

function ListTypeConverter(Data: TObject): TListOfObjects;
var
  List: TList<TObject>;
begin
  List := TList<TObject>(Data);
  Result := TListOfObjects(List.List);
  SetLength(Result, List.Count); // makes unique copy
end;

{ ... }

marshaller.RegisterConverter(TEmployeeList, ListTypeConverter);
marshaller.RegisterConverter(TDepartmentList, ListTypeConverter);
marshaller.RegisterConverter(TWorktimeList, ListTypeConverter);

Как видите, это такой же труд, как и использование атрибутов JsonReflect. Теоретически вы можете регистрировать преобразователи динамически на основе RTTI, но это может быть затруднительно из-за вложенных типов и наследования.

Подводя итог, можно сказать, что хотя библиотека Delphi JSON предоставляет способы подключения к процессу маршалинга, она не очень гибкая и поддерживает только самые базовые сценарии (см. также недавний вопрос Сериализация Spring4D Nullable JSON).

Последнее, что я хотел бы отметить, это то, что вам следует уделять больше внимания условностям. Поля записей/классов должны иметь префикс F по соглашению. Библиотека Delphi JSON учитывает это и удаляет F в процессе сериализации. Сериализация type TData = class Foo: string; end; в JSON дает: {"oo":""}.

Мне каким-то образом удалось заставить его работать, используя почти тот же метод, но динамический, поэтому мне не нужно иметь жесткую связь между моим конвертером json и всеми объектами во всей моей кодовой базе. Что касается вашего последнего замечания, это была ошибка копирования, и я обновил пример в своем вопросе. Я создам самостоятельный ответ с помощью моего нового конвертера, но он все еще не совсем работает, но он дает мне 95%, он по-прежнему отказывается вызывать конвертер в объекте списка верхнего уровня, но все остальное преобразуется, как я и ожидаю.

Matt Baech 26.09.2023 09:51
Ответ принят как подходящий

Вот кое-что, что работает - по какой-то причине это не работает, когда объект верхнего уровня представляет собой список, но пока я могу с этим смириться. Он использует RTTI и некоторые сомнительные методы поиска списков в объекте, но у меня это работает — в некоторой степени.

Если ObjectToConvert является потомком TList<T>, он все равно вернет json как объект со свойством listHelper, содержащим элементы списка.

Но только для объекта верхнего уровня

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

procedure FindObjectListsInHierarchy(obj: TObject; var result: TList<TClass>);

  function IsListDescendant(clazz:TClass) : boolean;
  begin
    result := false;
    while clazz <> nil do
    begin
      if clazz.ClassName.StartsWith('TList<') then
         exit(true);
      clazz := clazz.ClassParent;
    end;
  end;

begin
  if not assigned(result) then
    Result := TList<TClass>.Create;

  var ctx := TRttiContext.Create;
  var objType := ctx.GetType(obj.ClassType);

  for var prop in objType.GetProperties do
  begin
    if not ((prop.Visibility = mvPublic) or (prop.Visibility = mvPublished)) then
      continue;

    if prop.PropertyType.TypeKind in [tkClass, tkDynarray] then
    begin
      if IsListDescendant(TRttiInstanceType(Prop.Parent).MetaclassType) then
      begin
        if not result.Contains(obj.ClassType) then
          Result.Add(obj.ClassType);

        for var item in TList(Obj) do
          FindObjectListsInHierarchy(item, result);
      end;

      if prop.PropertyType.TypeKind = tkClass then
        FindObjectListsInHierarchy(prop.GetValue(obj).AsObject, result);
    end;
  end;
end;

function TMyJsonConverter.TypeObjectsConverter(Data: TObject): TListOfObjects;
begin
  var list := TList<TObject>(Data);
  Result := TListOfObjects(list.List);
  SetLength(Result, list.Count);
end;

function TMyJsonConverter.JsonConvert(ObjectToConvert: TObject): string;
begin
  var marshaller := TJSONMarshal.Create;
  try
    var listObjects: TList<TClass> := nil;
    try
      FindObjectListsInHierarchy(ObjectToConvert, listObjects);
      for var o in listObjects do
      begin
        marshaller.RegisterJSONMarshalled(o, 'FOwnsObjects', false);
        marshaller.RegisterConverter(o, TypeObjectsConverter);
      end;
    finally
      listObjects.Free;
    end;

    var json := marshaller.Marshal(ObjectToConvert);
    result := json.ToString;
    json.Free;
  finally
    marshaller.Free;
  end;
end;

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

Peter Wolf 26.09.2023 10:44

Вы обновили исходный пост новым вопросом, который, на мой взгляд, должен быть вынесен в отдельный пост. В любом случае, ответ на этот вопрос заключается в том, что библиотека Delphi JSON несовершенна и будет работать только в том случае, если вы зарегистрируете перехватчик, используя атрибут JsonReflect с преобразователем ctTypeObjects в вашем случае, например: [JsonReflect(ctTypeObjects, rtTypeObjects, TMyJsonInterceptor)] TEmployeeList = class(TObjectList<TEmployee>);. С учетом сказанного, если вам действительно нужно использовать библиотеку Delphi JSON, я бы рекомендовал вам пометить все ваши типы пользовательских списков атрибутом JsonReflect, и это все, что вам нужно сделать.

Peter Wolf 26.09.2023 10:44

@PeterWolf Мой первоначальный вопрос заключался в том, как мне заставить конвертер работать конкретно с потомками TObjectList<T>, то, что у меня была ошибка при регистрации конвертера, было просто частью решения. JsonInterceptor снова оказался ошибкой с моей стороны, поскольку я увидел, что у TJsonInterceptor есть виртуальные методы, соответствующие необходимым процедурам преобразования, теперь я пропустил это и соответствующим образом обновлю свой ответ. Я не буду использовать атрибуты для сотен или тысяч классов в многомиллионной базе кода, поскольку я не вижу никакой пользы от этих классов для хранения конкретных знаний о сериализации.

Matt Baech 26.09.2023 11:40

@PeterWolf Я ДЕЙСТВИТЕЛЬНО понимаю, что атрибуты - это предполагаемый способ работы, но это не решает мой вариант использования. Поэтому этот вопрос.

Matt Baech 26.09.2023 11:41

В том-то и дело - с TJSONMarshal это невозможно. Вы можете проверить это самостоятельно в методе TTypeMarshaller<TSerial>.MarshalData (System.JsonReflect.pas). После проверки значения nil он вызывает метод GetTypeConverter для разрешения перехватчика типа, объявленного атрибутом JsonReflect. Если его нет, он игнорирует любые зарегистрированные преобразователи и переходит непосредственно к маршалингу полей типа.

Peter Wolf 26.09.2023 12:15

@PeterWolf Но решение, которое я опубликовал, работает - у меня сейчас нерабочее время, поэтому я не буду сейчас просматривать код. Но по какой-то причине опубликованный мной код работает и преобразует списки в массивы, которые можно сортировать.

Matt Baech 26.09.2023 20:21

Этот комментарий был связан с регистрацией конвертера для объектов верхнего уровня.

Peter Wolf 27.09.2023 08:47

Другие вопросы по теме