unit Myc.Data.Types.JSON; interface uses System.SysUtils, System.JSON, Myc.Data.Types; type // Provides methods for serializing and deserializing data values to/from JSON using the TDataType helpers. TDataJsonConverter = class public // Serializes a data value to a TJSONValue. class function ValueToJson(const Value: TDataType.TValue): TJSONValue; // Deserializes a TJSONValue to a data value based on the provided data type. class function JsonToValue(const Json: TJSONValue; const DataType: TDataType): TDataType.TValue; end; implementation uses System.Rtti, System.TypInfo, System.DateUtils, System.Math; { TDataJsonConverter } class function TDataJsonConverter.ValueToJson(const Value: TDataType.TValue): TJSONValue; var jsonObject: TJSONObject; jsonArray: TJSONArray; i: Integer; begin if not Assigned(Value.DataValue) then begin Result := TJSONNull.Create; Exit; end; case Value.DataType.Kind of dkVoid: Result := TJSONNull.Create; dkOrdinal: Result := TJSONNumber.Create(Value.AsOrdinal.Value); dkFloat: Result := TJSONNumber.Create(Value.AsFloat.Value); dkText: Result := TJSONString.Create(Value.AsText.Value); dkTimestamp: Result := TJSONString.Create(DateToISO8601(Value.AsTimestamp.Value, true)); dkDecimal: begin // Serialize as string to preserve precision. var decValue := Value.AsDecimal; var val := decValue.Value; var scale := decValue.DataType.Scale; var s: string; if (scale = 0) then s := IntToStr(val) else begin s := IntToStr(Abs(val)); if (Length(s) <= scale) then s := StringOfChar('0', scale - Length(s) + 1) + s; s := s.Insert(Length(s) - scale, '.'); if (val < 0) then s := '-' + s; end; Result := TJSONString.Create(s); end; dkEnum: begin var enumValue := Value.AsEnum; Result := TJSONString.Create(enumValue.DataType.Identifiers[enumValue.Value]); end; dkRecord: begin var recordValue := Value.AsRecord; var recordType := recordValue.DataType; jsonObject := TJSONObject.Create; Result := jsonObject; for i := 0 to recordType.FieldCount - 1 do begin jsonObject.AddPair( recordType.Fields[i].Name, ValueToJson(recordValue.Items[i]) // Items property returns TDataType.TValue ); end; end; dkTuple: begin var tupleValue := Value.AsTuple; jsonArray := TJSONArray.Create; Result := jsonArray; for i := 0 to tupleValue.ItemCount - 1 do jsonArray.AddElement(ValueToJson(tupleValue.Items[i])); end; dkArray: begin var arrayValue := Value.AsArray; jsonArray := TJSONArray.Create; Result := jsonArray; for i := 0 to arrayValue.ElementCount - 1 do jsonArray.AddElement(ValueToJson(arrayValue.Items[i])); end; dkVector: begin var vectorValue := Value.AsVector; jsonArray := TJSONArray.Create; Result := jsonArray; for i := 0 to vectorValue.ElementCount - 1 do jsonArray.AddElement(ValueToJson(vectorValue.Items[i])); end; dkMethod: raise ENotSupportedException.Create('Cannot serialize a method type to JSON.'); else raise ENotSupportedException.CreateFmt('Unsupported data kind: %s', [GetEnumName(TypeInfo(TDataKind), Ord(Value.DataType.Kind))]); end; end; class function TDataJsonConverter.JsonToValue(const Json: TJSONValue; const DataType: TDataType): TDataType.TValue; var jsonObject: TJSONObject; jsonArray: TJSONArray; items: TArray; i, idx: Integer; val: Int64; begin Assert(Assigned(DataType.DataType), 'DataType cannot be nil for deserialization'); case DataType.Kind of dkVoid: if (Json is TJSONNull) then Result := TDataType.Void.Value else raise EConvertError.Create('JSON Null expected for Void type'); dkOrdinal: Result := DataType.AsOrdinal.CreateValue((Json as TJSONNumber).AsInt64); dkFloat: Result := DataType.AsFloat.CreateValue((Json as TJSONNumber).AsDouble); dkText: Result := DataType.AsText.CreateValue((Json as TJSONString).Value); dkTimestamp: Result := DataType.AsTimestamp.CreateValue(ISO8601ToDate((Json as TJSONString).Value, True)); dkDecimal: begin // Handle string representation for precision, with fallback to number for compatibility. var decimalType := DataType.AsDecimal; if (Json is TJSONString) then begin var s := (Json as TJSONString).Value.Trim; var isNegative := s.StartsWith('-'); if isNegative then s := s.Remove(0, 1); // remove sign var dotPos := Pos('.', s); var numFracDigits: Integer; if (dotPos = 0) then numFracDigits := 0 else begin numFracDigits := Length(s) - dotPos; s := s.Remove(dotPos - 1, 1); // remove dot end; val := StrToInt64(s); var scaleDiff := decimalType.Scale - numFracDigits; if (scaleDiff > 0) then // Not enough fractional digits begin for i := 1 to scaleDiff do val := val * 10; end else if (scaleDiff < 0) then // Too many fractional digits begin for i := 1 to -scaleDiff do val := val div 10; end; if isNegative then val := -val; Result := decimalType.CreateValue(val); end else if (Json is TJSONNumber) then begin // Support numbers for compatibility, but this can lead to precision loss. val := Round((Json as TJSONNumber).AsDouble * Power(10, decimalType.Scale)); Result := decimalType.CreateValue(val); end else raise EConvertError.Create('JSON String or Number expected for Decimal type'); end; dkEnum: begin var enumType := DataType.AsEnum; idx := enumType.IndexOf((Json as TJSONString).Value); if (idx < 0) then raise EConvertError.CreateFmt('Invalid enum identifier "%s" for type %s', [Json.Value, DataType.Name]); Result := enumType.CreateValue(idx); end; dkRecord: begin var recordType := DataType.AsRecord; jsonObject := Json as TJSONObject; SetLength(items, recordType.FieldCount); for i := 0 to recordType.FieldCount - 1 do begin var field := recordType.Fields[i]; var jsonPair := jsonObject.Get(field.Name); if not Assigned(jsonPair) then raise EConvertError.CreateFmt('Missing field "%s" for record type %s', [field.Name, DataType.Name]); // Recursive call returns TDataType.TValue, which implicitly converts to IDataValue for the array items[i] := JsonToValue(jsonPair.JsonValue, field.DataType); end; Result := recordType.CreateValue(items); end; dkTuple: begin var tupleType := DataType.AsTuple; jsonArray := Json as TJSONArray; if (jsonArray.Count <> IDataTupleType(tupleType).ItemCount) then raise EConvertError .CreateFmt('JSON array size (%d) does not match tuple size (%d)', [jsonArray.Count, tupleType.ItemCount]); SetLength(items, IDataTupleType(tupleType).ItemCount); for i := 0 to IDataTupleType(tupleType).ItemCount - 1 do begin var itemType := IDataTupleType(tupleType).ItemType[i]; items[i] := JsonToValue(jsonArray.Items[i], itemType); end; Result := tupleType.CreateValue(items); end; dkArray: begin var arrayType := DataType.AsArray; jsonArray := Json as TJSONArray; SetLength(items, jsonArray.Count); for i := 0 to jsonArray.Count - 1 do items[i] := JsonToValue(jsonArray.Items[i], arrayType.ElementType); Result := arrayType.CreateValue(items); end; dkVector: begin var vectorType := DataType.AsVector; jsonArray := Json as TJSONArray; if (jsonArray.Count <> vectorType.ElementCount) then raise EConvertError .CreateFmt('JSON array size (%d) does not match vector size (%d)', [jsonArray.Count, vectorType.ElementCount]); SetLength(items, jsonArray.Count); for i := 0 to jsonArray.Count - 1 do items[i] := JsonToValue(jsonArray.Items[i], vectorType.ElementType); Result := vectorType.CreateValue(items); end; dkMethod: raise ENotSupportedException.Create('Cannot deserialize a method type from JSON.'); else raise ENotSupportedException.CreateFmt('Unsupported data kind: %s', [GetEnumName(TypeInfo(TDataKind), Ord(DataType.Kind))]); end; end; end.