diff --git a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs index 13b5f92f00..5570dca794 100644 --- a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs +++ b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs @@ -133,6 +133,7 @@ internal static async Task EvaluateExpression_internal( ExpressionFunction.multiply => Multiply(args), ExpressionFunction.divide => Divide(args), ExpressionFunction.list => List(args), + ExpressionFunction.@object => Object(args), ExpressionFunction.INVALID => throw new ExpressionEvaluatorTypeErrorException( $"Function {expr.Args.FirstOrDefault()} not implemented in backend {expr}" ), @@ -1004,6 +1005,11 @@ private static JsonArray List(ExpressionValue[] args) return new JsonArray(args.Select(a => JsonSerializer.SerializeToNode(a)).ToArray()); } + private static JsonObject Object(ExpressionValue[] args) + { + return new ObjectFunctionEvaluator(args).Evaluate(); + } + /// /// Performs arithmetic operation using decimal precision to avoid floating point precision issues. /// Converts doubles to decimal, performs the operation, and converts back to double. diff --git a/src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs b/src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs index 113141fc2d..f2ca6354ae 100644 --- a/src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs +++ b/src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Globalization; +using System.Numerics; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; @@ -19,7 +20,7 @@ namespace Altinn.App.Core.Internal.Expressions; // double is a value type where nullable takes extra space, and we only read it when it should be set private readonly double _numberValue = 0; - // private readonly Dictionary? _objectValue = null; + private readonly JsonObject? _objectValue = null; private readonly JsonArray? _arrayValue = null; /// @@ -84,11 +85,12 @@ private ExpressionValue(string? value) _stringValue = value; } - // private ExpressionValue(Dictionary? value) - // { - // _valueKind = value is null ? JsonValueKind.Null : JsonValueKind.Object; - // _objectValue = value; - // } + /// Constructor for object value + public ExpressionValue(JsonObject value) + { + ValueKind = JsonValueKind.Object; + _objectValue = value; + } /// Constructor for array value public ExpressionValue(JsonArray value) @@ -112,10 +114,10 @@ public ExpressionValue(JsonArray value) /// public static implicit operator ExpressionValue(string? value) => new(value); - // /// - // /// Convert a Dictionary to ExpressionValue - // /// - // public static implicit operator ExpressionValue(Dictionary? value) => new(value); + /// + /// Convert a Dictionary to ExpressionValue + /// + public static implicit operator ExpressionValue(JsonObject value) => new(value); /// /// Convert an array to ExpressionValue @@ -174,17 +176,20 @@ public static ExpressionValue FromObject(object? value) '"' ) // Trim quotes to match the string representation , - JsonArray jsonArrayValue => jsonArrayValue, - _ => ToJsonArrayOrNull(value), + BigInteger => Null, + JsonObject jsonObject => jsonObject, + JsonArray jsonArray => jsonArray, + _ => ToJsonNodeOrNull(value), }; } - private static ExpressionValue ToJsonArrayOrNull(object? value) + private static ExpressionValue ToJsonNodeOrNull(object? value) { var node = JsonSerializer.SerializeToNode(value); return node switch { JsonArray jsonArray => jsonArray, + JsonObject jsonObject => jsonObject, _ => Null, }; } @@ -202,7 +207,7 @@ private static ExpressionValue ToJsonArrayOrNull(object? value) JsonValueKind.False => false, JsonValueKind.String => String, JsonValueKind.Number => Number, - // JsonValueKind.Object => Object, + JsonValueKind.Object => Dictionary, JsonValueKind.Array => Array, _ => throw new InvalidOperationException("Invalid value kind"), }; @@ -249,14 +254,15 @@ private static ExpressionValue ToJsonArrayOrNull(object? value) ), }; - // public Dictionary Object => - // _valueKind switch - // { - // JsonValueKind.Object => _objectValue ?? throw new UnreachableException($"{this} is not an object"), - // _ => throw new InvalidCastException( - // $"The .Object property can't be used on an expression value that represent a {_valueKind}" - // ), - // }; + /// Get the value as an object (or throw if it isn't an object ValueKind) + public JsonObject Dictionary => + ValueKind switch + { + JsonValueKind.Object => _objectValue ?? throw new UnreachableException($"{this} is not an object"), + _ => throw new InvalidCastException( + $"The .Object property can't be used on an expression value that represent a {ValueKind}" + ), + }; /// Get the value as an array (or throw if it isn't an array ValueKind) public JsonArray Array => @@ -280,8 +286,8 @@ public override string ToString() => JsonValueKind.False => "false", JsonValueKind.String => JsonSerializer.Serialize(String, _unsafeSerializerOptionsForSerializingDates), JsonValueKind.Number => Number.ToString(CultureInfo.InvariantCulture), - // JsonValueKind.Object => JsonSerializer.Serialize(Object), - // JsonValueKind.Array => JsonSerializer.Serialize(Array), + JsonValueKind.Object => JsonSerializer.Serialize(Dictionary), + JsonValueKind.Array => JsonSerializer.Serialize(Array), _ => throw new InvalidOperationException($"Invalid value kind {ValueKind}"), }; @@ -330,8 +336,8 @@ public override string ToString() => { } sValue => sValue, }, JsonValueKind.Number => Number.ToString(CultureInfo.InvariantCulture), - // JsonValueKind.Object => JsonSerializer.Serialize(Object), - // JsonValueKind.Array => JsonSerializer.Serialize(Array), + JsonValueKind.Object => JsonSerializer.Serialize(Dictionary), + JsonValueKind.Array => JsonSerializer.Serialize(Array), _ => throw new NotImplementedException($"ToStringForEquals not implemented for {ValueKind}"), }; @@ -634,7 +640,7 @@ public override ExpressionValue Read(ref Utf8JsonReader reader, Type typeToConve JsonTokenType.String => reader.GetString(), JsonTokenType.Number => reader.GetDouble(), JsonTokenType.Null => ExpressionValue.Null, - // JsonTokenType.StartObject => ReadObject(ref reader), + JsonTokenType.StartObject => ReadObject(ref reader, options), JsonTokenType.StartArray => ReadArray(ref reader, options), _ => throw new JsonException(), }; @@ -652,10 +658,17 @@ private static ExpressionValue ReadArray(ref Utf8JsonReader reader, JsonSerializ return new ExpressionValue(values); } - // private ExpressionValue ReadObject(ref Utf8JsonReader reader) - // { - // throw new NotImplementedException(); - // } + private static ExpressionValue ReadObject(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Expected StartObject token."); + } + var values = + JsonSerializer.Deserialize(ref reader, options) + ?? throw new JsonException("Expected EndObject token."); + return new ExpressionValue(values); + } /// public override void Write(Utf8JsonWriter writer, ExpressionValue value, JsonSerializerOptions options) @@ -678,9 +691,9 @@ public override void Write(Utf8JsonWriter writer, ExpressionValue value, JsonSer case JsonValueKind.Number: writer.WriteNumberValue(value.Number); break; - // case JsonValueKind.Object: - // JsonSerializer.Serialize(writer, value.Object, options); - // break; + case JsonValueKind.Object: + JsonSerializer.Serialize(writer, value.Dictionary, options); + break; case JsonValueKind.Array: JsonSerializer.Serialize(writer, value.Array, options); break; diff --git a/src/Altinn.App.Core/Internal/Expressions/ObjectFunctionEvaluator.cs b/src/Altinn.App.Core/Internal/Expressions/ObjectFunctionEvaluator.cs new file mode 100644 index 0000000000..084c47a8f9 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Expressions/ObjectFunctionEvaluator.cs @@ -0,0 +1,57 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Altinn.App.Core.Internal.Expressions; + +internal class ObjectFunctionEvaluator +{ + private readonly ExpressionValue[] _args; + + public ObjectFunctionEvaluator(ExpressionValue[] args) => _args = args; + + public JsonObject Evaluate() + { + AssertEvenNumberOfArguments(); + string[] keys = ExtractKeys(); + AssertKeysAreUnique(keys); + JsonNode?[] values = ExtractValues(); + Dictionary keyValuePairs = DictionaryFromKeysAndValues(keys, values); + return new JsonObject(keyValuePairs); + } + + private void AssertEvenNumberOfArguments() + { + if (_args.Length % 2 == 1) + { + throw new ExpressionEvaluatorTypeErrorException( + "The object function must have an even number of arguments." + ); + } + } + + private string[] ExtractKeys() + { + try + { + return _args.Where((_, index) => index % 2 == 0).Select(v => v.String).ToArray(); + } + catch (InvalidCastException) + { + throw new ExpressionEvaluatorTypeErrorException("Object keys must be strings."); + } + } + + private static void AssertKeysAreUnique(string[] keys) + { + if (keys.Length != keys.Distinct().Count()) + { + throw new ExpressionEvaluatorTypeErrorException("Object keys must be unique."); + } + } + + private JsonNode?[] ExtractValues() => + _args.Where((_, index) => index % 2 == 1).Select(v => JsonSerializer.SerializeToNode(v)).ToArray(); + + private static Dictionary DictionaryFromKeysAndValues(string[] keys, JsonNode?[] values) => + keys.Zip(values, (k, v) => new { k, v }).ToDictionary(x => x.k, x => x.v); +} diff --git a/src/Altinn.App.Core/Models/Expressions/ExpressionFunction.cs b/src/Altinn.App.Core/Models/Expressions/ExpressionFunction.cs index 66050fe549..1cba5d5964 100644 --- a/src/Altinn.App.Core/Models/Expressions/ExpressionFunction.cs +++ b/src/Altinn.App.Core/Models/Expressions/ExpressionFunction.cs @@ -228,4 +228,9 @@ public enum ExpressionFunction /// Create a list from the arguments. list, + + /// Create a dictionary from the arguments, which must be alternating keys and values. +#pragma warning disable CA1720 + @object, +#pragma warning restore CA1720 } diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs index 514199bf09..79109a168b 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs @@ -232,6 +232,11 @@ public async Task Divide_Theory(string testName, ExpressionTestCaseRoot.TestCase public async Task List_Theory(string testName, ExpressionTestCaseRoot.TestCaseItem testCaseItem) => await RunTestCase(testName, new ExpressionTestCaseRoot(testCaseItem)); + [Theory] + [SharedTestCases("object")] + public async Task Object_Theory(string testName, ExpressionTestCaseRoot.TestCaseItem testCaseItem) => + await RunTestCase(testName, new ExpressionTestCaseRoot(testCaseItem)); + private static async Task LoadTestCase(string file, string folder) { ExpressionTestCaseRoot testCase = new(); diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/lookup-list.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/lookup-list.json index 51bd0b4c55..ac0d079a69 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/lookup-list.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/lookup-list.json @@ -12,8 +12,9 @@ "booleanList": [true, false], "nullList": [null, null], "multidimensionalList": [[[1, 2], [3, 4]], [[5, 6], [7, 8]]], + "objectList": [{ "a": 1 }, { "a": 2 }], "emptyList": [], - "differentTypesList": [1, "string", true, null, [null]], + "differentTypesList": [1, "string", true, null, [null], { "a": null }], "listThatLooksLikeAnExpression": ["equals", 1, 1] } } @@ -44,6 +45,11 @@ "expression": ["dataModel", "multidimensionalList"], "expects": [[[1, 2], [3, 4]], [[5, 6], [7, 8]]] }, + { + "name": "Object list lookup", + "expression": ["dataModel", "objectList"], + "expects": [{ "a": 1 }, { "a": 2 }] + }, { "name": "Empty list lookup", "expression": ["dataModel", "emptyList"], @@ -52,7 +58,7 @@ { "name": "Lookup of list with different types", "expression": ["dataModel", "differentTypesList"], - "expects": [1, "string", true, null, [null]] + "expects": [1, "string", true, null, [null], { "a": null }] }, { "name": "Lookup of list that looks like an expression", diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/lookup-object.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/lookup-object.json new file mode 100644 index 0000000000..be33e164fc --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/lookup-object.json @@ -0,0 +1,104 @@ +{ + "name": "Lookup an object", + "dataModels": [ + { + "dataElement": { + "id": "00dd7417-5b4e-402a-bb73-007537071f1d", + "dataType": "default" + }, + "data": { + "simpleCity": { + "name": "Oslo", + "numberOfInhabitants": 724290, + "isCapital": true, + "largestVolcano": null + }, + "complexCity": { + "name": "Oslo", + "trainStations": [ + { + "name": "Oslo sentralstasjon", + "numberOfPlatforms": 19, + "isUnderground": false, + "expressTrains": ["F1", "F4", "F5", "F6"], + "connectedBusTerminal": "Oslo bussterminal" + }, + { + "name": "Nationaltheatret", + "numberOfPlatforms": 4, + "isUnderground": true, + "expressTrains": ["F5"], + "connectedBusTerminal": null + } + ] + } + } + } + ], + "testCases": [ + { + "name": "Simple object lookup", + "expression": ["dataModel", "simpleCity"], + "expects": { + "name": "Oslo", + "numberOfInhabitants": 724290, + "isCapital": true, + "largestVolcano": null + } + }, + { + "name": "Complex object lookup", + "expression": ["dataModel", "complexCity"], + "expects": { + "name": "Oslo", + "trainStations": [ + { + "name": "Oslo sentralstasjon", + "numberOfPlatforms": 19, + "isUnderground": false, + "expressTrains": ["F1", "F4", "F5", "F6"], + "connectedBusTerminal": "Oslo bussterminal" + }, + { + "name": "Nationaltheatret", + "numberOfPlatforms": 4, + "isUnderground": true, + "expressTrains": ["F5"], + "connectedBusTerminal": null + } + ] + } + }, + { + "name": "Query returning an object", + "expression": ["dataModel", "complexCity.trainStations[0]"], + "expects": { + "name": "Oslo sentralstasjon", + "numberOfPlatforms": 19, + "isUnderground": false, + "expressTrains": ["F1", "F4", "F5", "F6"], + "connectedBusTerminal": "Oslo bussterminal" + } + }, + { + "name": "Query returning a list of objects", + "expression": ["dataModel", "complexCity.trainStations"], + "expects": [ + { + "name": "Oslo sentralstasjon", + "numberOfPlatforms": 19, + "isUnderground": false, + "expressTrains": ["F1", "F4", "F5", "F6"], + "connectedBusTerminal": "Oslo bussterminal" + }, + { + "name": "Nationaltheatret", + "numberOfPlatforms": 4, + "isUnderground": true, + "expressTrains": ["F5"], + "connectedBusTerminal": null + } + ] + } + ] +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/object-is-null.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/object-is-null.json deleted file mode 100644 index 7b520e9ef6..0000000000 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/object-is-null.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Looking up an object returns null", - "expression": ["dataModel", "a"], - "expects": null, - "dataModels": [ - { - "dataElement": { - "id": "00dd7417-5b4e-402a-bb73-007537071f1d", - "dataType": "default" - }, - "data": { - "a": { - "value": "ABC" - } - } - } - ], - "layouts": { - "Page1": { - "$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/layout/layout.schema.v1.json", - "data": { - "layout": [ - { - "id": "current-component", - "type": "Paragraph" - } - ] - } - } - }, - "context": { - "component": "current-component", - "currentLayout": "Page1" - } -} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/list/list.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/list/list.json index 01efa321cf..5103e81ac4 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/list/list.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/list/list.json @@ -21,6 +21,11 @@ "expression": ["list", ["list", 1, 2], ["list", 3, 4]], "expects": [[1, 2], [3, 4]] }, + { + "name": "Supports objects", + "expression": ["list", ["object", "a", 1], ["object", "b", 2]], + "expects": [{ "a": 1 }, { "b": 2 }] + }, { "name": "Does not evaluate the final expression when the result happens to be a valid expression", "expression": ["list", "equals", 1, 1], diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/object/object.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/object/object.json new file mode 100644 index 0000000000..0ebf3d3c30 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/object/object.json @@ -0,0 +1,59 @@ +{ + "name": "Object tests", + "testCases": [ + { + "name": "Creates an object from its arguments", + "expression": [ + "object", + "name", + "Oslo", + "numberOfInhabitants", + 724290, + "isCapital", + true, + "largestVolcano", + null + ], + "expects": { + "name": "Oslo", + "numberOfInhabitants": 724290, + "isCapital": true, + "largestVolcano": null + } + }, + { + "name": "Supports empty objects", + "expression": ["object"], + "expects": {} + }, + { + "name": "Supports nested objects", + "expression": ["object", "city", ["object", "name", "Oslo"]], + "expects": { + "city": { "name": "Oslo" } + } + }, + { + "name": "Supports lists", + "expression": ["object", "cities", ["list", "Oslo", "Leikanger", "Brønnøysund"]], + "expects": { + "cities": ["Oslo", "Leikanger", "Brønnøysund"] + } + }, + { + "name": "Fails when an odd number of arguments is passed", + "expression": ["object", "name", "Oslo", "numberOfInhabitants"], + "expectsFailure": "The object function must have an even number of arguments" + }, + { + "name": "Fails when a value that is not a string is used as key", + "expression": ["object", 1, "a"], + "expectsFailure": "Object keys must be strings" + }, + { + "name": "Fails when there are duplicate keys", + "expression": ["object", "name", "Oslo", "numberOfInhabitants", 724290, "name", "Christiania"], + "expectsFailure": "Object keys must be unique" + } + ] +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/EqualsTests.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/EqualsTests.cs index 2a973e0484..8a25e96945 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/EqualsTests.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/EqualsTests.cs @@ -90,13 +90,6 @@ public void ToStringForEquals_AgreesWithJsonSerializer(object? value) new() { new BigInteger(123), // Not supported by JsonSerializer, but might make sense to support - new object(), - new - { - A = 1, - B = 2, - C = 3, - }, }; [Theory] diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/ExpressionValueTests.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/ExpressionValueTests.cs index d9ef86cc27..8940d9a082 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/ExpressionValueTests.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/ExpressionValueTests.cs @@ -162,7 +162,12 @@ public void TestFromObject() [InlineData("[]")] [InlineData("[1,2,3]")] [InlineData("[[[1,2],[3,4]],[[5,6],[7,8]]]")] - [InlineData("[1,\"test\",true,null,[]]")] + [InlineData("[1,\"test\",true,null,[],{}]")] + [InlineData("{}")] + [InlineData("{\"a\":1,\"b\":\"test\",\"c\":true,\"d\":null,\"e\":[]}")] + [InlineData("{\"a\":{\"b\":1}}")] + [InlineData("{\"a\":[1,2,3]}")] + [InlineData("[{\"a\":1},{\"b\":2}]")] public void TestJsonParsing(string json) { ExpressionValue value = JsonSerializer.Deserialize(json); @@ -181,6 +186,7 @@ public void TestUndefined() Assert.Throws(() => undefinedValue.Number); Assert.Throws(() => undefinedValue.String); Assert.Throws(() => undefinedValue.Array); + Assert.Throws(() => undefinedValue.Dictionary); Assert.Equal("null", JsonSerializer.Serialize(undefinedValue)); Assert.Throws(() => undefinedValue.GetHashCode()); @@ -198,21 +204,7 @@ public void NullThrowsWhenAccessedAsDifferentType() Assert.Throws(() => _ = nullValue.Number); Assert.Throws(() => _ = nullValue.String); Assert.Throws(() => _ = nullValue.Array); - } - - [Fact] - public void TestObjectsFail() - { - // This is probably temporary - Assert.Throws(() => - { - JsonSerializer.Deserialize("{\"key\": \"value\"}"); - }); - - Assert.Throws(() => - { - JsonSerializer.Deserialize("{\"key\": 123}"); - }); + Assert.Throws(() => _ = nullValue.Dictionary); } [Fact] diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index 849412f527..e86317f85b 100644 --- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -3229,13 +3229,15 @@ namespace Altinn.App.Core.Internal.Expressions public ExpressionEvaluatorTypeErrorException(string msg, System.Exception innerException) { } } [System.Diagnostics.DebuggerDisplay("{ToString(),nq}")] - [System.Text.Json.Serialization.JsonConverter(typeof(Altinn.App.Core.Internal.Expressions.ExpressionTypeUnionConverter?))] + [System.Text.Json.Serialization.JsonConverter(typeof(Altinn.App.Core.Internal.Expressions.ExpressionTypeUnionConverter))] public readonly struct ExpressionValue : System.IEquatable { public ExpressionValue() { } public ExpressionValue(System.Text.Json.Nodes.JsonArray value) { } + public ExpressionValue(System.Text.Json.Nodes.JsonObject value) { } public System.Text.Json.Nodes.JsonArray Array { get; } public bool Bool { get; } + public System.Text.Json.Nodes.JsonObject Dictionary { get; } public double Number { get; } public string String { get; } public System.Text.Json.JsonValueKind ValueKind { get; } @@ -3255,6 +3257,7 @@ namespace Altinn.App.Core.Internal.Expressions public bool TryDeserialize(out T? result) { } public static Altinn.App.Core.Internal.Expressions.ExpressionValue FromObject(object? value) { } public static Altinn.App.Core.Internal.Expressions.ExpressionValue op_Implicit(System.Text.Json.Nodes.JsonArray value) { } + public static Altinn.App.Core.Internal.Expressions.ExpressionValue op_Implicit(System.Text.Json.Nodes.JsonObject value) { } public static Altinn.App.Core.Internal.Expressions.ExpressionValue op_Implicit(bool? value) { } public static Altinn.App.Core.Internal.Expressions.ExpressionValue op_Implicit(double? value) { } public static Altinn.App.Core.Internal.Expressions.ExpressionValue op_Implicit(string? value) { } @@ -4721,6 +4724,7 @@ namespace Altinn.App.Core.Models.Expressions multiply = 39, divide = 40, list = 41, + @object = 42, } } namespace Altinn.App.Core.Models.Layout.Components.Base