Я пытаюсь зарегистрировать собственный конвертер, чтобы избежать унаследованных свойств в 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>, все эти списки будут сериализованы как объекты со списком в качестве свойства.
@PeterWolf Я добавил некоторые данные, которые могут показать мою проблему :)
Отвечает ли это на ваш вопрос? Как скрыть свойства TObjectList «ownsObjects» и «listHelper» из Json с помощью Delphi (Rest.JSON)?
К сожалению, нет, я внимательно прочитал этот ответ, и именно это натолкнуло меня на путь создания пользовательского конвертера. Моя проблема в том, что я хочу, чтобы это был общий конвертер для всех моих объектов. И я должен сказать, что считаю некрасивым аннотировать все мои модели атрибутами JsonReflect. По сути, я хочу сделать то, что сделал Стефан в своем ответе, но без атрибута.
@RemyLebeau Спасибо за очистку, однако я не ответил на вопрос, как уже упоминалось, теперь он называется, но все еще не работает должным образом.
@MattBaech См. функцию ListTypeConverter и ее регистрацию в моем ответе. Например, единая регистрация преобразователя типов для TEmployeeList будет применяться как к полю Employees класса TMySampleDTO, так и к полю Employees класса TDepartment.

В документации для TTypeMarshaller.RegisterConverter отсутствует какая-либо подробная информация, и у вас нет другого выбора, кроме как изучить исходный код Delphi RTL. На первый взгляд все перегруженные версии метода можно разделить на 3 группы:
TConverterEvent — регистрирует преобразователь, который выполняет преобразование на основе своего свойства ConverterType . Это свойство доступно только для чтения, но его значение устанавливается вместе с заданием любого из его делегатов преобразования (*Converter свойств).T...Converter — регистрирует преобразователь для FieldName (2-й параметр) внутри типа класса clazz (1-й параметр). Назовем его «преобразователем поля».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%, он по-прежнему отказывается вызывать конвертер в объекте списка верхнего уровня, но все остальное преобразуется, как я и ожидаю.
Вот кое-что, что работает - по какой-то причине это не работает, когда объект верхнего уровня представляет собой список, но пока я могу с этим смириться. Он использует 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, который вы, похоже, игнорируете.
Вы обновили исходный пост новым вопросом, который, на мой взгляд, должен быть вынесен в отдельный пост. В любом случае, ответ на этот вопрос заключается в том, что библиотека Delphi JSON несовершенна и будет работать только в том случае, если вы зарегистрируете перехватчик, используя атрибут JsonReflect с преобразователем ctTypeObjects в вашем случае, например: [JsonReflect(ctTypeObjects, rtTypeObjects, TMyJsonInterceptor)] TEmployeeList = class(TObjectList<TEmployee>);. С учетом сказанного, если вам действительно нужно использовать библиотеку Delphi JSON, я бы рекомендовал вам пометить все ваши типы пользовательских списков атрибутом JsonReflect, и это все, что вам нужно сделать.
@PeterWolf Мой первоначальный вопрос заключался в том, как мне заставить конвертер работать конкретно с потомками TObjectList<T>, то, что у меня была ошибка при регистрации конвертера, было просто частью решения. JsonInterceptor снова оказался ошибкой с моей стороны, поскольку я увидел, что у TJsonInterceptor есть виртуальные методы, соответствующие необходимым процедурам преобразования, теперь я пропустил это и соответствующим образом обновлю свой ответ. Я не буду использовать атрибуты для сотен или тысяч классов в многомиллионной базе кода, поскольку я не вижу никакой пользы от этих классов для хранения конкретных знаний о сериализации.
@PeterWolf Я ДЕЙСТВИТЕЛЬНО понимаю, что атрибуты - это предполагаемый способ работы, но это не решает мой вариант использования. Поэтому этот вопрос.
В том-то и дело - с TJSONMarshal это невозможно. Вы можете проверить это самостоятельно в методе TTypeMarshaller<TSerial>.MarshalData (System.JsonReflect.pas). После проверки значения nil он вызывает метод GetTypeConverter для разрешения перехватчика типа, объявленного атрибутом JsonReflect. Если его нет, он игнорирует любые зарегистрированные преобразователи и переходит непосредственно к маршалингу полей типа.
@PeterWolf Но решение, которое я опубликовал, работает - у меня сейчас нерабочее время, поэтому я не буду сейчас просматривать код. Но по какой-то причине опубликованный мной код работает и преобразует списки в массивы, которые можно сортировать.
Этот комментарий был связан с регистрацией конвертера для объектов верхнего уровня.
Не могли бы вы предоставить пример структуры данных, которую вы пытаетесь сериализовать в JSON?