Skip to content

Commit fcb24f1

Browse files
committed
Fix Flattening of table values in JSON function for untype object
1 parent 5a41f67 commit fcb24f1

2 files changed

Lines changed: 121 additions & 1 deletion

File tree

src/libraries/Microsoft.PowerFx.Json/Functions/JsonFunctionImpl.cs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,12 +414,46 @@ private void Visit(IUntypedObject untypedObject, int depth = 0)
414414
{
415415
_writer.WriteStartArray();
416416

417+
// Check if this is a single-column "Value" table that should be flattened
418+
var shouldFlatten = false;
419+
if (_flattenValueTables && untypedObject.GetArrayLength() > 0)
420+
{
421+
// Check if the first element is an object with only one property named "Value"
422+
for (var i = 0; i < untypedObject.GetArrayLength(); i++)
423+
{
424+
var element = untypedObject[i];
425+
if (element.Type is ExternalType firstExternalType &&
426+
(firstExternalType.Kind == ExternalTypeKind.Object || firstExternalType.Kind == ExternalTypeKind.ArrayAndObject) &&
427+
element.TryGetPropertyNames(out IEnumerable<string> propertyNames))
428+
{
429+
var propList = propertyNames.ToList();
430+
if (propList.Count == 1 && propList[0] == TexlFunction.ColumnName_ValueStr)
431+
{
432+
shouldFlatten = true;
433+
break;
434+
}
435+
}
436+
}
437+
}
438+
417439
for (var i = 0; i < untypedObject.GetArrayLength(); i++)
418440
{
419441
CheckLimitsAndCancellation(depth);
420442

421443
IUntypedObject row = untypedObject[i];
422-
Visit(row, depth + 1);
444+
445+
if (shouldFlatten
446+
&& row.TryGetProperty(TexlFunction.ColumnName_ValueStr, out IUntypedObject value)
447+
&& row.TryGetPropertyNames(out var propNames)
448+
&& propNames.Count() == 1)
449+
{
450+
// Flatten by visiting just the value
451+
Visit(value, depth + 1);
452+
}
453+
else
454+
{
455+
Visit(row, depth + 1);
456+
}
423457
}
424458

425459
_writer.WriteEndArray();

src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/JsonSerializeUOTests.cs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,92 @@ public async Task JsonSerializeUOTest()
6666
yield return (20, new TestUO(123456789012345.6789012345678m), @"""123456789012345.6789012345678""");
6767
}
6868

69+
[Fact]
70+
public async Task JsonSerializeUOWithFlattenValueTablesTest()
71+
{
72+
PowerFxConfig config = new PowerFxConfig();
73+
config.EnableJsonFunctions();
74+
75+
SymbolTable symbolTable = new SymbolTable();
76+
ISymbolSlot objSlot = symbolTable.AddVariable("obj", FormulaType.UntypedObject);
77+
RecalcEngine engine = new RecalcEngine(config);
78+
79+
// Test 1: Single-column "Value" table should be flattened
80+
var singleColumnValueArray = new[]
81+
{
82+
new { Value = 1 },
83+
new { Value = 2 },
84+
new { Value = 3 }
85+
};
86+
87+
SymbolValues symbolValues1 = new SymbolValues(symbolTable);
88+
symbolValues1.Set(objSlot, FormulaValue.New(new TestUO(singleColumnValueArray)));
89+
RuntimeConfig runtimeConfig1 = new RuntimeConfig(symbolValues1);
90+
91+
// With FlattenValueTables - should flatten to [1,2,3]
92+
FormulaValue fv1 = await engine.EvalAsync("JSON(obj, JSONFormat.FlattenValueTables)", CancellationToken.None, runtimeConfig: runtimeConfig1);
93+
Assert.IsNotType<ErrorValue>(fv1);
94+
string str1 = fv1.ToExpression().ToString();
95+
Assert.Equal(@"""[1,2,3]""", str1);
96+
97+
// Without FlattenValueTables - should keep structure
98+
FormulaValue fv2 = await engine.EvalAsync("JSON(obj)", CancellationToken.None, runtimeConfig: runtimeConfig1);
99+
Assert.IsNotType<ErrorValue>(fv2);
100+
string str2 = fv2.ToExpression().ToString();
101+
Assert.Equal(@"""[{""""Value"""":1},{""""Value"""":2},{""""Value"""":3}]""", str2);
102+
103+
// Test 2: Multi-column table should NOT be flattened even with FlattenValueTables
104+
var multiColumnArray = new[]
105+
{
106+
new { a = 1, b = 2 },
107+
new { a = 3, b = 4 }
108+
};
109+
110+
SymbolValues symbolValues2 = new SymbolValues(symbolTable);
111+
symbolValues2.Set(objSlot, FormulaValue.New(new TestUO(multiColumnArray)));
112+
RuntimeConfig runtimeConfig2 = new RuntimeConfig(symbolValues2);
113+
114+
FormulaValue fv3 = await engine.EvalAsync("JSON(obj, JSONFormat.FlattenValueTables)", CancellationToken.None, runtimeConfig: runtimeConfig2);
115+
Assert.IsNotType<ErrorValue>(fv3);
116+
string str3 = fv3.ToExpression().ToString();
117+
Assert.Equal(@"""[{""""a"""":1,""""b"""":2},{""""a"""":3,""""b"""":4}]""", str3);
118+
119+
// Test 3: Test with null/blank values in Value column
120+
var arrayWithNulls = new[]
121+
{
122+
new { Value = (int?)1 },
123+
new { Value = (int?)null },
124+
new { Value = (int?)3 }
125+
};
126+
127+
SymbolValues symbolValues3 = new SymbolValues(symbolTable);
128+
symbolValues3.Set(objSlot, FormulaValue.New(new TestUO(arrayWithNulls)));
129+
RuntimeConfig runtimeConfig3 = new RuntimeConfig(symbolValues3);
130+
131+
FormulaValue fv4 = await engine.EvalAsync("JSON(obj, JSONFormat.FlattenValueTables)", CancellationToken.None, runtimeConfig: runtimeConfig3);
132+
Assert.IsNotType<ErrorValue>(fv4);
133+
string str4 = fv4.ToExpression().ToString();
134+
Assert.Equal(@"""[1,null,3]""", str4);
135+
136+
// Test 4: Mixed array with multi-column object first, then single-column Value objects
137+
var mixedArray = new object[]
138+
{
139+
new { Value = 2, Value2 = 22 },
140+
new { Value = 1 },
141+
new { Value = 3 }
142+
};
143+
144+
SymbolValues symbolValues4 = new SymbolValues(symbolTable);
145+
symbolValues4.Set(objSlot, FormulaValue.New(new TestUO(mixedArray)));
146+
RuntimeConfig runtimeConfig4 = new RuntimeConfig(symbolValues4);
147+
148+
FormulaValue fv5 = await engine.EvalAsync("JSON(obj, JSONFormat.FlattenValueTables)", CancellationToken.None, runtimeConfig: runtimeConfig4);
149+
Assert.IsNotType<ErrorValue>(fv5);
150+
string str5 = fv5.ToExpression().ToString();
151+
// Should flatten only the single-property Value objects, keep the multi-property object as-is
152+
Assert.Equal(@"""[{""""Value"""":2,""""Value2"""":22},1,3]""", str5);
153+
}
154+
69155
public class TestUO : IUntypedObject
70156
{
71157
private enum UOType

0 commit comments

Comments
 (0)