From 9639b333f897330a3e30a296bc23e69de11bb504 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Tue, 28 Jul 2026 13:28:31 +0530 Subject: [PATCH 1/3] test(mssql-json): add OpenAPI + GraphQL schema-discovery tests (Phase 3b) Proves the 'JSON is treated as a normal string' contract at the schema-discovery layer, complementing the merged REST round-trip tests (#3720). - JsonTypeSchemaTests (T008): the profiles.metadata json column is exposed in the OpenAPI document as type:string, nullable, with no bespoke format. - MsSqlGraphQLJsonSchemaTests (T009): GraphQL introspection reports Profile.metadata as the built-in nullable String scalar (no custom JSON scalar). MCP describe_entities (T010) is intentionally omitted: describe_entities projects only config field name/description, not DB column data types, so there is no JSON-specific behavior to assert there. --- .../OpenApiDocumentor/JsonTypeSchemaTests.cs | 67 +++++++++++++++++++ .../MsSqlGraphQLJsonSchemaTests.cs | 60 +++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 src/Service.Tests/OpenApiDocumentor/JsonTypeSchemaTests.cs create mode 100644 src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLJsonSchemaTests.cs diff --git a/src/Service.Tests/OpenApiDocumentor/JsonTypeSchemaTests.cs b/src/Service.Tests/OpenApiDocumentor/JsonTypeSchemaTests.cs new file mode 100644 index 0000000000..0c83758f13 --- /dev/null +++ b/src/Service.Tests/OpenApiDocumentor/JsonTypeSchemaTests.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Azure.DataApiBuilder.Config.ObjectModel; +using Microsoft.OpenApi.Models; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Azure.DataApiBuilder.Service.Tests.OpenApiIntegration +{ + /// + /// Validates how a SQL Server 2025 native json column is surfaced in the generated + /// OpenAPI document. DAB does nothing special for a JSON column - it is treated as a normal + /// string, so it must be described with type: string, no custom format, + /// and honoring the column's nullability. This is the schema-discovery counterpart to the + /// REST round-trip coverage in + /// . + /// NOTE: The native JSON data type requires SQL Server 2025 / Azure SQL. + /// + [TestCategory(TestCategory.MSSQL)] + [TestClass] + public class JsonTypeSchemaTests + { + private const string CONFIG_FILE = "json-type-openapi-config.MsSql.json"; + private const string DB_ENV = TestCategory.MSSQL; + + /// + /// The profiles.metadata (json, nullable) column must appear in the OpenAPI schema as + /// a plain nullable string with no format - proving JSON is not given a bespoke scalar/format. + /// + [TestMethod] + public async Task JsonColumn_IsDescribedAsNullableStringWithoutFormat() + { + OpenApiDocument doc = await GenerateProfileDocumentAsync(); + + Assert.IsTrue(doc.Components.Schemas.ContainsKey("Profile"), "Schema should exist for the Profile entity."); + + OpenApiSchema profileSchema = doc.Components.Schemas["Profile"]; + Assert.IsTrue(profileSchema.Properties.ContainsKey("metadata"), "The json 'metadata' column should be present in the schema."); + + OpenApiSchema metadataSchema = profileSchema.Properties["metadata"]; + Assert.AreEqual("string", metadataSchema.Type, "A json column must be described as a plain string (treated like any string column)."); + Assert.IsTrue(string.IsNullOrEmpty(metadataSchema.Format), "A json column must not carry a bespoke OpenAPI format."); + Assert.IsTrue(metadataSchema.Nullable, "The nullable json column must be described as nullable."); + } + + /// + /// Builds an OpenAPI document for a Profile entity sourced from the profiles table, + /// with REST + GraphQL enabled and anonymous/authenticated CRUD permissions. + /// + private static async Task GenerateProfileDocumentAsync() + { + Entity entity = new( + Source: new("profiles", EntitySourceType.Table, null, null), + Fields: null, + GraphQL: new("Profile", "Profiles", true), + Rest: new(EntityRestOptions.DEFAULT_SUPPORTED_VERBS), + Permissions: OpenApiTestBootstrap.CreateBasicPermissions(), + Mappings: null, + Relationships: null); + + RuntimeEntities entities = new(new Dictionary { { "Profile", entity } }); + return await OpenApiTestBootstrap.GenerateOpenApiDocumentAsync(entities, CONFIG_FILE, DB_ENV); + } + } +} diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLJsonSchemaTests.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLJsonSchemaTests.cs new file mode 100644 index 0000000000..b84dfdf8a7 --- /dev/null +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLJsonSchemaTests.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Azure.DataApiBuilder.Service.Tests.SqlTests.GraphQLQueryTests +{ + /// + /// GraphQL schema-discovery (introspection) tests for a SQL Server 2025 native json column. + /// DAB does nothing special for a JSON column - it is treated as a normal string, so the + /// GraphQL schema must expose it using the built-in String scalar (no bespoke JSON scalar), + /// honoring the column's nullability. This is the GraphQL counterpart to the REST/OpenAPI coverage + /// in and JsonTypeSchemaTests. + /// NOTE: The native JSON data type requires SQL Server 2025 / Azure SQL. + /// + [TestClass, TestCategory(TestCategory.MSSQL)] + public class MsSqlGraphQLJsonSchemaTests : SqlTestBase + { + [ClassInitialize] + public static async Task SetupAsync(TestContext context) + { + DatabaseEngine = TestCategory.MSSQL; + await InitializeTestFixture(); + } + + /// + /// Introspecting the Profile type must report its json-backed metadata field as the + /// built-in nullable String scalar - proving JSON gets no custom scalar in the schema. + /// + [TestMethod] + public async Task JsonColumn_IsIntrospectedAsBuiltInStringScalar() + { + string introspectionQuery = @"{ + __type(name: ""Profile"") { + name + fields { + name + type { kind name ofType { kind name } } + } + } + }"; + + JsonElement type = await ExecuteGraphQLRequestAsync(introspectionQuery, "__type", isAuthenticated: false); + + Assert.AreEqual("Profile", type.GetProperty("name").GetString(), "Introspection should resolve the Profile GraphQL type."); + + JsonElement metadataField = type.GetProperty("fields").EnumerateArray() + .Single(f => f.GetProperty("name").GetString() == "metadata"); + + // A nullable column surfaces as the bare scalar (no NON_NULL wrapper), so type.kind/name + // describe the scalar directly. + JsonElement metadataType = metadataField.GetProperty("type"); + Assert.AreEqual("SCALAR", metadataType.GetProperty("kind").GetString(), "A json column must map to a scalar, not a custom object/scalar type."); + Assert.AreEqual("String", metadataType.GetProperty("name").GetString(), "A json column must use the built-in String scalar (no bespoke JSON scalar)."); + } + } +} From bc69acd202580533da19d34511b590dd650b2d15 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Tue, 28 Jul 2026 18:10:57 +0530 Subject: [PATCH 2/3] test(mssql-json): drop OpenAPI Nullable assertion (not expressed by documentor) CI failure: JsonColumn Nullable assertion failed. DAB's OpenApiDocumentor builds each column property schema with only Type/Format/Description/Items and never sets Nullable for any column (nullability is expressed via request-body required lists, not the property schema). The assertion was wrong and not JSON-specific. Keep the meaningful 'nothing special' assertions: type==string and no format. Renamed the test to JsonColumn_IsDescribedAsStringWithoutFormat. --- .../OpenApiDocumentor/JsonTypeSchemaTests.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Service.Tests/OpenApiDocumentor/JsonTypeSchemaTests.cs b/src/Service.Tests/OpenApiDocumentor/JsonTypeSchemaTests.cs index 0c83758f13..e0cfa84ea5 100644 --- a/src/Service.Tests/OpenApiDocumentor/JsonTypeSchemaTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/JsonTypeSchemaTests.cs @@ -12,9 +12,8 @@ namespace Azure.DataApiBuilder.Service.Tests.OpenApiIntegration /// /// Validates how a SQL Server 2025 native json column is surfaced in the generated /// OpenAPI document. DAB does nothing special for a JSON column - it is treated as a normal - /// string, so it must be described with type: string, no custom format, - /// and honoring the column's nullability. This is the schema-discovery counterpart to the - /// REST round-trip coverage in + /// string, so it must be described with type: string and no custom format. + /// This is the schema-discovery counterpart to the REST round-trip coverage in /// . /// NOTE: The native JSON data type requires SQL Server 2025 / Azure SQL. /// @@ -26,23 +25,24 @@ public class JsonTypeSchemaTests private const string DB_ENV = TestCategory.MSSQL; /// - /// The profiles.metadata (json, nullable) column must appear in the OpenAPI schema as - /// a plain nullable string with no format - proving JSON is not given a bespoke scalar/format. + /// The profiles.metadata (json) column must appear in the OpenAPI schema as a plain + /// string with no format - proving JSON is not given a bespoke scalar/format and is treated + /// exactly like any other string column. (DAB''s OpenAPI documentor does not express column + /// nullability on the property schema for any type, so that is not asserted here.) /// [TestMethod] - public async Task JsonColumn_IsDescribedAsNullableStringWithoutFormat() + public async Task JsonColumn_IsDescribedAsStringWithoutFormat() { OpenApiDocument doc = await GenerateProfileDocumentAsync(); Assert.IsTrue(doc.Components.Schemas.ContainsKey("Profile"), "Schema should exist for the Profile entity."); OpenApiSchema profileSchema = doc.Components.Schemas["Profile"]; - Assert.IsTrue(profileSchema.Properties.ContainsKey("metadata"), "The json 'metadata' column should be present in the schema."); + Assert.IsTrue(profileSchema.Properties.ContainsKey("metadata"), "The json ''metadata'' column should be present in the schema."); OpenApiSchema metadataSchema = profileSchema.Properties["metadata"]; Assert.AreEqual("string", metadataSchema.Type, "A json column must be described as a plain string (treated like any string column)."); Assert.IsTrue(string.IsNullOrEmpty(metadataSchema.Format), "A json column must not carry a bespoke OpenAPI format."); - Assert.IsTrue(metadataSchema.Nullable, "The nullable json column must be described as nullable."); } /// From 2f714e76b5cb2d6651faf6820b0a6f9564707e5d Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Wed, 29 Jul 2026 20:34:01 +0530 Subject: [PATCH 3/3] fix(mssql-json): return native json as string in read path + address review Per #2768, a JSON column must be treated as a string for input AND output. But the MSSQL read path (FOR JSON PATH) was inlining a native json column as a nested JSON object, so REST returned an object and a GraphQL read of the String-typed field threw a GraphQLMapping error (String leaf resolver calls JsonElement.GetString(), which fails on an object). Engine: MsSqlQueryBuilder.WrappedColumns now casts a native json column (SqlDbType.Json) to NVARCHAR(MAX) so FOR JSON PATH emits it as an escaped JSON string. Mutations already return json as a string via the tabular OUTPUT clause, so no change was needed there. Regular nvarchar columns are untouched. Tests (addresses aaronburtle review on #3738): - Flip MsSqlRestJsonTypesTests back to the string contract (ParseMetadata/GetJsonTypeList assert JsonValueKind.String). - Add GraphQL end-to-end read (profile_by_pk metadata) asserting the payload returns as a JSON string - guards against introspection passing while a real read throws. - OpenAPI test now asserts metadata is type:string with no format in the response AND the POST/PUT-PATCH request-body components (Profile_NoAutoPK/Profile_NoPK). --- src/Core/Resolvers/MsSqlQueryBuilder.cs | 29 ++++++++++++- .../OpenApiDocumentor/JsonTypeSchemaTests.cs | 41 ++++++++++++------- .../MsSqlGraphQLJsonSchemaTests.cs | 26 ++++++++++++ .../RestApiTests/MsSqlRestJsonTypesTests.cs | 30 +++++++------- 4 files changed, 95 insertions(+), 31 deletions(-) diff --git a/src/Core/Resolvers/MsSqlQueryBuilder.cs b/src/Core/Resolvers/MsSqlQueryBuilder.cs index 7769dcf94b..118857e701 100644 --- a/src/Core/Resolvers/MsSqlQueryBuilder.cs +++ b/src/Core/Resolvers/MsSqlQueryBuilder.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Data; using System.Data.Common; using System.Text; using System.Text.RegularExpressions; @@ -456,10 +457,36 @@ private string WrappedColumns(SqlQueryStructure structure) structure.Columns.Select( c => structure.IsSubqueryColumn(c) ? WrapSubqueryColumn(c, structure.JoinQueries[c.TableAlias!]) + $" AS {QuoteIdentifier(c.Label)}" : - Build(c) + BuildResultColumn(c, structure) )); } + /// + /// Builds a top-level (non-subquery) result column. + /// A SQL Server native json column is cast to NVARCHAR(MAX) so that the trailing + /// FOR JSON PATH clause emits it as an escaped JSON string instead of inlining it as a nested + /// JSON value. DAB treats a json column as a normal string, so its raw JSON text must + /// round-trip as a string at the REST/GraphQL boundary. + /// + private string BuildResultColumn(LabelledColumn column, SqlQueryStructure structure) + { + if (IsJsonColumn(column, structure)) + { + return $"CAST({Build(column as Column)} AS NVARCHAR(MAX)) AS {QuoteIdentifier(column.Label)}"; + } + + return Build(column); + } + + /// + /// Returns true when the given column is backed by a SQL Server native json column. + /// + private static bool IsJsonColumn(LabelledColumn column, SqlQueryStructure structure) + { + return structure.GetUnderlyingSourceDefinition().Columns.TryGetValue(column.ColumnName, out ColumnDefinition? columnDefinition) + && columnDefinition.SqlDbType == SqlDbType.Json; + } + /// /// Builds the parameter list for the stored procedure execute call /// paramKeys are the user-generated procedure parameter names diff --git a/src/Service.Tests/OpenApiDocumentor/JsonTypeSchemaTests.cs b/src/Service.Tests/OpenApiDocumentor/JsonTypeSchemaTests.cs index e0cfa84ea5..914a63cef7 100644 --- a/src/Service.Tests/OpenApiDocumentor/JsonTypeSchemaTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/JsonTypeSchemaTests.cs @@ -11,9 +11,10 @@ namespace Azure.DataApiBuilder.Service.Tests.OpenApiIntegration { /// /// Validates how a SQL Server 2025 native json column is surfaced in the generated - /// OpenAPI document. DAB does nothing special for a JSON column - it is treated as a normal - /// string, so it must be described with type: string and no custom format. - /// This is the schema-discovery counterpart to the REST round-trip coverage in + /// OpenAPI document. DAB treats a json column as a normal string for input and output, + /// so it must be described with type: string and no custom format in the response + /// schema as well as the POST / PUT / PATCH request-body schemas. This is the schema-discovery + /// counterpart to the REST round-trip coverage in /// . /// NOTE: The native JSON data type requires SQL Server 2025 / Azure SQL. /// @@ -25,24 +26,36 @@ public class JsonTypeSchemaTests private const string DB_ENV = TestCategory.MSSQL; /// - /// The profiles.metadata (json) column must appear in the OpenAPI schema as a plain - /// string with no format - proving JSON is not given a bespoke scalar/format and is treated - /// exactly like any other string column. (DAB''s OpenAPI documentor does not express column - /// nullability on the property schema for any type, so that is not asserted here.) + /// The profiles.metadata (json) column must be described as a plain string with no format + /// in the response schema and in both request-body schemas (POST => Profile_NoAutoPK, + /// PUT/PATCH => Profile_NoPK) - proving json is treated like a normal string for both input + /// and output, with no bespoke scalar/format. (DAB does not express column nullability on the + /// property schema for any type, so that is not asserted here.) /// [TestMethod] - public async Task JsonColumn_IsDescribedAsStringWithoutFormat() + public async Task JsonColumn_IsDescribedAsStringWithoutFormat_InResponseAndRequestBodies() { OpenApiDocument doc = await GenerateProfileDocumentAsync(); - Assert.IsTrue(doc.Components.Schemas.ContainsKey("Profile"), "Schema should exist for the Profile entity."); + // Response body schema, plus the POST and PUT/PATCH request-body schemas. + AssertMetadataIsPlainString(doc, "Profile"); + AssertMetadataIsPlainString(doc, "Profile_NoAutoPK"); + AssertMetadataIsPlainString(doc, "Profile_NoPK"); + } + + /// + /// Asserts the named component schema exposes metadata as a plain string with no format. + /// + private static void AssertMetadataIsPlainString(OpenApiDocument doc, string schemaName) + { + Assert.IsTrue(doc.Components.Schemas.ContainsKey(schemaName), $"Schema {schemaName} should exist."); - OpenApiSchema profileSchema = doc.Components.Schemas["Profile"]; - Assert.IsTrue(profileSchema.Properties.ContainsKey("metadata"), "The json ''metadata'' column should be present in the schema."); + OpenApiSchema schema = doc.Components.Schemas[schemaName]; + Assert.IsTrue(schema.Properties.ContainsKey("metadata"), $"The json metadata column should be present in {schemaName}."); - OpenApiSchema metadataSchema = profileSchema.Properties["metadata"]; - Assert.AreEqual("string", metadataSchema.Type, "A json column must be described as a plain string (treated like any string column)."); - Assert.IsTrue(string.IsNullOrEmpty(metadataSchema.Format), "A json column must not carry a bespoke OpenAPI format."); + OpenApiSchema metadataSchema = schema.Properties["metadata"]; + Assert.AreEqual("string", metadataSchema.Type, $"A json column must be described as a plain string in {schemaName}."); + Assert.IsTrue(string.IsNullOrEmpty(metadataSchema.Format), $"A json column must not carry a bespoke OpenAPI format in {schemaName}."); } /// diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLJsonSchemaTests.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLJsonSchemaTests.cs index b84dfdf8a7..66ee6156a7 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLJsonSchemaTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLJsonSchemaTests.cs @@ -56,5 +56,31 @@ public async Task JsonColumn_IsIntrospectedAsBuiltInStringScalar() Assert.AreEqual("SCALAR", metadataType.GetProperty("kind").GetString(), "A json column must map to a scalar, not a custom object/scalar type."); Assert.AreEqual("String", metadataType.GetProperty("name").GetString(), "A json column must use the built-in String scalar (no bespoke JSON scalar)."); } + + /// + /// profile_by_pk(id: 1) { metadata } - Verify that reading the json-backed field through GraphQL + /// succeeds and returns the payload as a JSON string. The String leaf resolver calls + /// JsonElement.GetString(), which only works because the engine casts the json column to + /// NVARCHAR(MAX) so it is emitted as an escaped string rather than an inlined JSON object. + /// This guards against the introspection test passing while a real read throws a GraphQLMapping error. + /// + [TestMethod] + public async Task JsonColumn_GraphQLRead_ReturnsPayloadAsString() + { + string query = @"{ + profile_by_pk(id: 1) { + metadata + } + }"; + + JsonElement result = await ExecuteGraphQLRequestAsync(query, "profile_by_pk", isAuthenticated: false); + + JsonElement metadata = result.GetProperty("metadata"); + Assert.AreEqual(JsonValueKind.String, metadata.ValueKind, "A json column must be returned as a JSON string through GraphQL (treated as a normal string)."); + + JsonElement parsed = JsonDocument.Parse(metadata.GetString()!).RootElement; + Assert.AreEqual("admin", parsed.GetProperty("role").GetString()); + Assert.AreEqual(3, parsed.GetProperty("tier").GetInt32()); + } } } diff --git a/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestJsonTypesTests.cs b/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestJsonTypesTests.cs index 8ead7d4d15..8e8c20417d 100644 --- a/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestJsonTypesTests.cs +++ b/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestJsonTypesTests.cs @@ -12,12 +12,10 @@ namespace Azure.DataApiBuilder.Service.Tests.SqlTests.RestApiTests { /// /// Tests for SQL Server native JSON column support via REST endpoints (read and write). - /// DAB does nothing special for a JSON column - it is written from the request payload and - /// read back with no custom handling, new scalar, or format. On read, SQL Server's - /// FOR JSON PATH projection (which DAB uses to shape every result) inlines a native json - /// column as a nested JSON value, so the metadata surfaces at the REST boundary as a JSON - /// object rather than an escaped string. Writes still supply the payload as a JSON string, - /// exactly as a normal string column would be written. + /// DAB treats a JSON column exactly like a string: the raw JSON text is written from the + /// request payload and read back as a JSON string (the MSSQL query builder casts the native + /// json column to NVARCHAR(MAX) so FOR JSON PATH emits it as an escaped string rather than + /// inlining it as a nested object). No new scalar or format is involved. /// Assertions compare the returned metadata semantically so they are robust to any /// whitespace / key-order normalization the engine applies to the JSON type. /// NOTE: The native JSON data type requires SQL Server 2025 / Azure SQL. @@ -38,7 +36,7 @@ public static async Task SetupAsync(TestContext context) /// /// GET /api/Profile - Verify the whole collection (5 seeded rows) is returned and that - /// each metadata value renders either as a native JSON object payload or null (row 5). + /// each metadata value renders either as a JSON string payload or null (row 5). /// [TestMethod] public async Task GetJsonTypeList() @@ -50,13 +48,13 @@ public async Task GetJsonTypeList() .RootElement.GetProperty("value"); Assert.AreEqual(5, items.GetArrayLength(), "Expected the 5 seeded profile rows."); - // Rows 1-4 carry a JSON payload (inlined as a native JSON object); row 5 is null. + // Rows 1-4 carry a JSON payload (returned as an escaped JSON string); row 5 is null. foreach (JsonElement record in items.EnumerateArray()) { JsonValueKind metadataKind = record.GetProperty("metadata").ValueKind; Assert.IsTrue( - metadataKind is JsonValueKind.Object or JsonValueKind.Null, - $"Expected metadata to be a JSON object or null, but was {metadataKind}."); + metadataKind is JsonValueKind.String or JsonValueKind.Null, + $"Expected metadata to be a JSON string or null, but was {metadataKind}."); } } @@ -248,19 +246,19 @@ private static async Task GetRecordByIdAsync(int id) } /// - /// Returns the metadata field as a JSON element. DAB applies no special handling to a JSON - /// column, so SQL Server's FOR JSON PATH projection inlines it as a native JSON object at the - /// REST boundary. This helper asserts the value is a JSON object and returns it for inspection. + /// Returns the metadata field parsed as a JSON element. DAB treats a JSON column as a string, + /// so a non-null metadata value arrives at the REST boundary as a JSON string carrying the + /// payload. This helper asserts that and parses the string payload for semantic inspection. /// private static JsonElement ParseMetadata(JsonElement record) { JsonElement metadata = record.GetProperty("metadata"); Assert.AreEqual( - JsonValueKind.Object, + JsonValueKind.String, metadata.ValueKind, - "A native JSON column is inlined as a JSON object at the REST boundary via FOR JSON PATH."); + "A json column must be returned as a JSON string at the REST boundary (treated as a normal string)."); - return metadata.Clone(); + return JsonDocument.Parse(metadata.GetString()!).RootElement.Clone(); } #endregion