{ *************************************************************************** Copyright (c) 2015-2018 Kike Pérez Unit : Quick.JSON.Serializer Description : Json Serializer Author : Kike Pérez Version : 1.0 Created : 21/05/2018 Modified : 08/06/2018 This file is part of QuickLib: https://github.com/exilon/QuickLib *************************************************************************** Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. *************************************************************************** } unit Quick.Json.Serializer; interface uses System.Classes, System.SysUtils, System.Rtti, System.TypInfo, System.Json, System.DateUtils, Quick.Commons; type EJsonSerializeError = class(Exception) end; TNotSerializableProperty = class(TCustomAttribute); TCommentProperty = class(TCustomAttribute) private fComment : string; public constructor Create(const aComment: string); property Comment : string read fComment; end; TCustomNameProperty = class(TCustomAttribute) private fName : string; public constructor Create(const aName: string); property Name : string read fName; end; IJsonSerializer = interface ['{CA26F7AE-F1FE-41BE-9C23-723A687F60D1}'] function JsonToObject(aType: TClass; const aJson: string): TObject; overload; function JsonToObject(aObject: TObject; const aJson: string): TObject; overload; function ObjectToJson(aObject: TObject): string; end; TJsonSerializer = class(TInterfacedObject,IJsonSerializer) strict private function GetValue(aAddr: Pointer; aType: TRTTIType): TValue; procedure DeserializeDynArray(aProperty : TRttiProperty; aObject : TObject; const aJsonArray: TJSONArray); function DeserializeRecord(aRecord : TRttiRecordType; aTypeInfo : PTypeInfo; aObject : TObject; const aJson : TJSONObject) : TValue; function DeserializeClass(aType : TClass; const aJson : TJSONObject) : TObject; function DeserializeObject(aObject : TObject; const aJson : TJSONObject) : TObject; overload; function DeserializeObject(aObject : TObject; const aName : string; aProperty : TRttiProperty; const aJson : TJSONObject) : TObject; overload; function DeserializeType(aObject : TObject; aType : TTypeKind; aTypeInfo : PTypeInfo; const aValue: string) : TValue; function Serialize(const aName : string; aValue : TValue) : TJSONPair; overload; function Serialize(aObject : TObject) : TJSONObject; overload; public function JsonToObject(aType : TClass; const aJson: string) : TObject; overload; function JsonToObject(aObject : TObject; const aJson: string) : TObject; overload; function ObjectToJson(aObject : TObject): string; end; PPByte = ^PByte; resourcestring cNotSupportedDataType = 'Not supported "%s" data type "%s"'; cNotSerializable = 'Object is not serializable'; implementation { TqlJsonSerializer } procedure TJsonSerializer.DeserializeDynArray(aProperty: TRttiProperty; aObject: TObject; const aJsonArray: TJSONArray); var rType: PTypeInfo; len: NativeInt; pArr: Pointer; rValue : TValue; rItemValue: TValue; i: Integer; objClass: TClass; ctx : TRttiContext; rRec : TRttiRecordType; s : string; begin if GetTypeData(aProperty.PropertyType.Handle).DynArrElType = nil then Exit; len := aJsonArray.Count; rType := GetTypeData(aProperty.PropertyType.Handle).DynArrElType^; pArr := nil; DynArraySetLength(pArr, aProperty.PropertyType.Handle, 1, @len); try TValue.Make(@pArr, aProperty.PropertyType.Handle, rValue); for i := 0 to aJsonArray.Count - 1 do begin rItemValue := nil; case rType.Kind of tkClass : begin if aJsonArray.Items[i] is TJSONObject then begin objClass := rType.TypeData.ClassType; rItemValue := DeserializeClass(objClass, TJSONObject(aJsonArray.Items[i])); end; end; tkRecord : begin {rRec := ctx.GetType(rType).AsRecord; try rItemValue := DeserializeRecord(rRec,aObject,TJSONObject(aJsonArray.Items[i])); finally ctx.Free; end;} end; tkMethod, tkPointer, tkClassRef ,tkInterface, tkProcedure : begin //skip these properties end else begin //raise EJsonSerializeError.Create(Format(cNotSupportedDataType,[aProperty.Name,GetTypeName(rType)])); rItemValue := DeserializeType(aObject,rType.Kind,aProperty.GetValue(aObject).TypeInfo,aJsonArray.Items[i].Value); end; end; if not rItemValue.IsEmpty then rValue.SetArrayElement(i,rItemValue); end; aProperty.SetValue(aObject,rValue); finally DynArrayClear(pArr, aProperty.PropertyType.Handle); end; end; function TJsonSerializer.DeserializeRecord(aRecord : TRttiRecordType; aTypeInfo : PTypeInfo; aObject : TObject; const aJson : TJSONObject) : TValue; var rField : TRttiField; rValue : TValue; member : TJSONPair; s : string; begin TValue.Make(@aRecord,aTypeInfo,Result); for rField in aRecord.GetFields do begin member := TJSONPair(aJson.GetValue(rField.Name)); if member <> nil then case rField.FieldType.TypeKind of tkDynArray : begin {jArray := TJSONObject.ParseJSONValue(member.ToJSON) as TJSONArray; try DeserializeDynArray(aProp,Result,jArray); finally jArray.Free; end;} end; tkClass : begin //if (member.JsonValue is TJSONObject) then {begin objClass := aProp.PropertyType.Handle^.TypeData.ClassType; rValue := DeserializeClass(objClass, TJSONObject.ParseJSONValue(member.ToJson) as TJSONObject); //aProp.SetValue(Result, rValue); end;} end; else begin s := member.JsonString.ToString; rValue := DeserializeType(aObject,rField.FieldType.TypeKind,rField.FieldType.Handle,member.JsonString.ToString); end; end; rField.SetValue(@aRecord,rValue); end; //Result := aRecord; end; function TJsonSerializer.JsonToObject(aObject: TObject; const aJson: string): TObject; var json: TJSONObject; begin json := TJSONObject.ParseJSONValue(aJson,True) as TJSONObject; try Result := DeserializeObject(aObject,json); finally json.Free; end; end; function TJsonSerializer.JsonToObject(aType: TClass; const aJson: string): TObject; var json: TJSONObject; begin json := TJSONObject.ParseJSONValue(aJson) as TJSONObject; try Result := DeserializeClass(aType,json); finally json.Free; end; end; function TJsonSerializer.ObjectToJson(aObject: TObject): string; var json: TJSONObject; begin json := Serialize(aObject); try Result := json.ToJSON; finally json.Free; end; end; function TJsonSerializer.DeserializeClass(aType: TClass; const aJson: TJSONObject): TObject; begin Result := nil; if aJson.Count = 0 then Exit; Result := aType.Create; try Result := DeserializeObject(Result,aJson); except Result.Free; raise; end; end; function TJsonSerializer.DeserializeObject(aObject: TObject; const aJson: TJSONObject): TObject; var ctx: TRttiContext; rType: TRttiType; rProp: TRttiProperty; attr: TCustomAttribute; rValue: TValue; NotSerializable: Boolean; propertyname : string; begin Result := aObject; if (aJson.Count = 0) or (Result = nil) then Exit; NotSerializable := True; try rType := ctx.GetType(aObject.ClassInfo); try for rProp in rType.GetProperties do begin if (rProp.PropertyType.IsPublicType) and (rProp.Name <> 'RefCount') then begin propertyname := rProp.Name; for attr in rProp.GetAttributes do if attr is TCustomNameProperty then propertyname := TCustomNameProperty(attr).Name; rValue := DeserializeObject(Result, propertyname, rProp, aJson); //rProp.SetValue(Result,rValue); end; NotSerializable := False; end; finally ctx.Free; end; if NotSerializable then begin raise EJsonSerializeError.Create(cNotSerializable); end; except Result.Free; raise; end; end; function TJsonSerializer.DeserializeObject(aObject : TObject; const aName : string; aProperty : TRttiProperty; const aJson : TJSONObject) : TObject; var rType : PTypeInfo; ctx : TRttiContext; rValue : TValue; member : TJSONPair; NotSerializable : Boolean; objClass: TClass; jArray : TJSONArray; rRec : TRttiRecordType; rField : TRttiField; begin Result := aObject; member := TJSONPair(aJson.GetValue(aName)); if member <> nil then begin case aProperty.PropertyType.TypeKind of tkDynArray : begin jArray := TJSONObject.ParseJSONValue(member.ToJSON) as TJSONArray; try DeserializeDynArray(aProperty,Result,jArray); finally jArray.Free; end; end; tkClass : begin //if (member.JsonValue is TJSONObject) then begin objClass := aProperty.PropertyType.Handle^.TypeData.ClassType; rValue := DeserializeClass(objClass, TJSONObject.ParseJSONValue(member.ToJson) as TJSONObject); end end; tkRecord : begin {rRec := ctx.GetType(aProperty.PropertyType.Handle).AsRecord; //rValue := GetValue(aProp.Handle,TRttiRecordType); //TValue.Make(@rRec,aProp.PropertyType.Handle, rValue); //rRec.GetField('IP').SetValue(@rRec,'127.0.0.2'); //rRec.GetField('MsgSize').SetValue(@rRec,1024); try rValue := DeserializeRecord(rRec,aProperty.GetValue(aObject).TypeInfo,aObject,TJSONObject.ParseJSONValue(member.ToJson) as TJSONObject); finally ctx.Free; end;} end; else begin rValue := DeserializeType(Result,aProperty.PropertyType.TypeKind,aProperty.GetValue(Result).TypeInfo,member.ToJSON); end; end; if not rValue.IsEmpty then aProperty.SetValue(Result,rValue); end; end; function TJsonSerializer.DeserializeType(aObject : TObject; aType : TTypeKind; aTypeInfo : PTypeInfo; const aValue: string) : TValue; var i : Integer; value : string; begin value := AnsiDequotedStr(aValue,'"'); case aType of tkString, tkLString, tkWString, tkUString : begin Result := value; end; tkChar, tkWChar : begin Result := value; end; tkInteger : begin Result := StrToInt(value); end; tkInt64 : begin Result := StrToInt64(value); end; tkFloat : begin if aTypeInfo = TypeInfo(TDateTime) then begin Result := JsonDateToDateTime(value); end else if aTypeInfo = TypeInfo(TDate) then begin Result := StrToDate(value); end else if aTypeInfo = TypeInfo(TTime) then begin Result := StrToTime(value); end else begin Result := StrToFloat(value); end; end; tkEnumeration : begin if aTypeInfo = System.TypeInfo(Boolean) then begin Result := StrToBool(value); end else begin TValue.Make(GetEnumValue(aTypeInfo,value),aTypeInfo, Result); end; end; tkSet : begin i := StringToSet(aTypeInfo,value); TValue.Make(@i,aTypeInfo,Result); end; else begin //raise EclJsonSerializerError.Create('Not supported data type!'); end; end; end; function TJsonSerializer.Serialize(aObject: TObject): TJSONObject; var ctx: TRttiContext; attr : TCustomAttribute; rType: TRttiType; rProp: TRttiProperty; NotSerializable: Boolean; jpair : TJSONPair; ExcludeSerialize : Boolean; comment : string; propertyname : string; begin if (aObject = nil) then begin Result := nil; Exit; end; NotSerializable := True; Result := TJSONObject.Create; try rType := ctx.GetType(aObject.ClassInfo); try //s := rType.ToString; for rProp in rType.GetProperties do begin ExcludeSerialize := False; comment := ''; propertyname := rProp.Name; for attr in rProp.GetAttributes do begin if attr is TNotSerializableProperty then ExcludeSerialize := True else if attr is TCommentProperty then comment := TCommentProperty(attr).Comment else if attr is TCustomNameProperty then propertyname := TCustomNameProperty(attr).Name; end; if (rProp.PropertyType.IsPublicType) and (rProp.Name <> 'RefCount') and (not ExcludeSerialize) then begin //add comment as pair if comment <> '' then Result.AddPair(TJSONPair.Create('#Comment#->'+propertyname,Comment)); //s := rProp.Name; jpair := Serialize(propertyname,rProp.GetValue(aObject)); //s := jpair.JsonValue.ToString; if jpair <> nil then Result.AddPair(jpair) else jpair.Free; //Result.AddPair(Serialize(rProp.Name,rProp.GetValue(aObject))); //s := Result.ToJSON; end; NotSerializable := False; end; finally ctx.Free; end; if NotSerializable then begin raise EJsonSerializeError.Create(cNotSerializable); end; except Result.Free; raise; end; end; function TJsonSerializer.GetValue(aAddr: Pointer; aType: TRTTIType): TValue; begin TValue.Make(aAddr,aType.Handle,Result); end; function TJsonSerializer.Serialize(const aName : string; aValue : TValue): TJSONPair; var ctx: TRttiContext; rRec : TRttiRecordType; rField : TRttiField; rDynArray : TRTTIDynamicArrayType; json : TJSONObject; jArray : TJSONArray; jPair : TJSONPair; jValue : TJSONValue; i : Integer; begin Result := TJSONPair.Create(aName,nil); //Result.JsonString := TJSONString(aName); try case aValue.Kind of tkDynArray : begin jArray := TJSONArray.Create; rDynArray := ctx.GetType(aValue.TypeInfo) as TRTTIDynamicArrayType; try for i := 0 to aValue.GetArrayLength - 1 do begin jPair := Serialize(aName,GetValue(PPByte(aValue.GetReferenceToRawData)^ + rDynArray.ElementType.TypeSize * i, rDynArray.ElementType)); try //jValue := TJsonValue(jPair.JsonValue.Clone); jValue := jPair.JsonValue; jArray.AddElement(jValue); jPair.JsonValue.Owned := False; finally jPair.Free; jValue.Owned := True; end; end; Result.JsonValue := jArray; finally ctx.Free; end; end; tkClass : begin Result.JsonValue := TJSONValue(Serialize(aValue.AsObject)); end; tkString, tkLString, tkWString, tkUString : begin Result.JsonValue := TJSONString.Create(aValue.AsString); end; tkChar, tkWChar : begin Result.JsonValue := TJSONString.Create(aValue.AsString); end; tkInteger : begin Result.JsonValue := TJSONNumber.Create(aValue.AsInteger); end; tkInt64 : begin Result.JsonValue := TJSONNumber.Create(aValue.AsInt64); end; tkFloat : begin if aValue.TypeInfo = TypeInfo(TDateTime) then begin Result.JsonValue := TJSONString.Create(DateTimeToJsonDate(aValue.AsExtended)); end else if aValue.TypeInfo = TypeInfo(TDate) then begin Result.JsonValue := TJSONString.Create(DateToStr(aValue.AsExtended)); end else if aValue.TypeInfo = TypeInfo(TTime) then begin Result.JsonValue := TJSONString.Create(TimeToStr(aValue.AsExtended)); end else Result.JsonValue := TJSONNumber.Create(aValue.AsExtended); end; tkEnumeration : begin if (aValue.TypeInfo = System.TypeInfo(Boolean)) then begin Result.JsonValue := TJSONBool.Create(aValue.AsBoolean); end else begin Result.JsonValue := TJSONString.Create(GetEnumName(aValue.TypeInfo,aValue.AsOrdinal)); //Result.JsonValue := TJSONString.Create(aValue.ToString); end; end; tkSet : begin Result.JsonValue := TJSONString.Create(aValue.ToString); end; tkRecord : begin rRec := ctx.GetType(aValue.TypeInfo).AsRecord; try json := TJSONObject.Create; for rField in rRec.GetFields do begin json.AddPair(Serialize(rField.name,rField.GetValue(aValue.GetReferenceToRawData))); end; Result.JsonValue := json; finally ctx.Free; end; end; tkMethod, tkPointer, tkClassRef ,tkInterface, tkProcedure : begin //skip these properties FreeAndNil(Result); end else begin raise EJsonSerializeError.Create(Format(cNotSupportedDataType,[aName,GetTypeName(aValue.TypeInfo)])); end; end; except Result.Free; raise; end; end; { TCommentProperty } constructor TCommentProperty.Create(const aComment: string); begin fComment := aComment; end; { TCustomNameProperty } constructor TCustomNameProperty.Create(const aName: string); begin fName := aName; end; end.