Skip to content

Commit 15fcfa5

Browse files
[Phase 2] Enable MSSQL JSON data type - type mapping & error codes (engine) (#3691)
## What this PR does Adds support for the SQL Server 2025 `JSON` column type by treating it exactly like a normal text (`string`) column. Reading, writing, filtering, and sorting all reuse the paths DAB already has for strings — no new type, no special handling. ## The change Two small edits to the engine: 1. **Map the type** — tell DAB that a `JSON` column is a `string`. 2. **Map the errors** — when SQL Server rejects invalid JSON, return **HTTP 400 (Bad Request)** instead of a generic 500. Both are covered by new unit tests. This PR is intentionally scoped to just the engine change. The database test table and end-to-end (REST/GraphQL) tests are **not** included here because they need a real SQL Server 2025 database — and our CI currently runs on LocalDB, which doesn't understand the `JSON` type yet. Those tests come in the next phase. ## Delivery plan | Phase | What | Status | |------|------|--------| | 1 | Upgrade to .NET 10 + SqlClient 6.x (prerequisite) | ✅ Merged (#3656) | | 2 | JSON type + error mapping | This PR | | 3 | Test table + full REST/GraphQL/MCP tests | Next | | 4 | Error/filter edge cases + regression | After | ## Notes - No changes to PostgreSQL, MySQL, DwSql, or CosmosDB. - Related issue: #2768. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 8b4ed96 commit 15fcfa5

5 files changed

Lines changed: 65 additions & 2 deletions

File tree

src/Core/Models/SqlTypeConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public static class SqlTypeConstants
4747
{ "time", true }, // SqlDbType.Time
4848
{ "datetime2", true }, // SqlDbType.DateTime2
4949
{ "datetimeoffset", true }, // SqlDbType.DateTimeOffset
50+
{ "json", true }, // SqlDbType.Json (SQL Server 2025+) treated as string
5051
{ "", false }, // SqlDbType.Udt and SqlDbType.Structured provided by SQL as empty strings (unsupported)
5152
{ "numeric", true}, // Not present in SqlDbType, however can be returned by sql functions like LAG and should map to decimal.
5253
{ "vector", true } // SqlDbType.Vector

src/Core/Resolvers/MsSqlDbExceptionParser.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ public MsSqlDbExceptionParser(RuntimeConfigProvider configProvider) : base(confi
3030
"4435", "4436", "4437", "4438", "4439", "4440", "4441",
3131
"4442", "4443", "4444", "4445", "4446", "4447", "4448",
3232
"4450", "4451", "4452", "4453", "4454", "4455", "4456",
33-
"4457", "4933", "4934", "4936", "4988", "8102"
33+
"4457", "4933", "4934", "4936", "4988", "8102",
34+
// JSON data type validation errors (SQL Server 2025+). Invalid JSON
35+
// supplied for a json column is a client error, mapped to HTTP 400.
36+
"13608", "13609", "13610", "13611", "13612", "13613", "13614"
3437
});
3538

3639
TransientExceptionCodes.UnionWith(new List<string>
@@ -92,8 +95,9 @@ public override DataApiBuilderException.SubStatusCodes GetResultSubStatusCodeFor
9295
{
9396
int errorCode = ((SqlException)e).Number;
9497
// MSSQL 201 - Procedure or function '%.*ls' expects parameter '%.*ls', which was not supplied.
98+
// SQL Server 2025+ JSON validation errors are also client input errors.
9599
// Pending revisions of error code classifications, this is a temporary fix to determine substatus code.
96-
if (errorCode == 201)
100+
if (errorCode is 201 or 13608 or 13609 or 13610 or 13611 or 13612 or 13613 or 13614)
97101
{
98102
return DataApiBuilderException.SubStatusCodes.DatabaseInputError;
99103
}

src/Core/Services/TypeHelper.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ public static class TypeHelper
100100
[SqlDbType.Float] = typeof(double),
101101
[SqlDbType.Image] = typeof(byte[]),
102102
[SqlDbType.Int] = typeof(int),
103+
[SqlDbType.Json] = typeof(string),
103104
[SqlDbType.Money] = typeof(decimal),
104105
[SqlDbType.NChar] = typeof(char),
105106
[SqlDbType.NText] = typeof(string),

src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,17 @@ public void ResolveUnderlyingTypeForNullableValueType(Type nullableType)
111111
Assert.AreNotEqual(notExpected: JsonDataType.Undefined, actual: TypeHelper.GetJsonDataTypeFromSystemType(nullableType));
112112
Assert.IsNotNull(TypeHelper.GetDbTypeFromSystemType(nullableType));
113113
}
114+
115+
/// <summary>
116+
/// Validates that the SQL Server 2025+ 'json' data type literal resolves to the
117+
/// CLR string type and maps to JsonDataType.String, i.e. DAB treats a JSON column
118+
/// just like a string column.
119+
/// </summary>
120+
[TestMethod]
121+
public void JsonSqlDbTypeResolvesToString()
122+
{
123+
Type resolvedType = TypeHelper.GetSystemTypeFromSqlDbType("json");
124+
Assert.AreEqual(typeof(string), resolvedType);
125+
Assert.AreEqual(JsonDataType.String, TypeHelper.GetJsonDataTypeFromSystemType(resolvedType));
126+
}
114127
}

src/Service.Tests/UnitTests/DbExceptionParserUnitTests.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Azure.DataApiBuilder.Config.ObjectModel;
1010
using Azure.DataApiBuilder.Core.Configurations;
1111
using Azure.DataApiBuilder.Core.Resolvers;
12+
using Azure.DataApiBuilder.Service.Exceptions;
1213
using Azure.DataApiBuilder.Service.Tests.SqlTests;
1314
using Microsoft.VisualStudio.TestTools.UnitTesting;
1415
using Moq;
@@ -94,5 +95,48 @@ public void TestIsTransientExceptionMethod(bool expected, int number)
9495

9596
Assert.AreEqual(expected, dbExceptionParser.IsTransientException(SqlTestHelper.CreateSqlException(number)));
9697
}
98+
99+
/// <summary>
100+
/// Validates that JSON data type validation error codes raised by SQL Server 2025+
101+
/// when invalid JSON is supplied for a json column are mapped to HTTP 400 Bad Request
102+
/// and are classified as client input errors.
103+
/// </summary>
104+
/// <param name="number">SQL error code populated in SqlException.Number.</param>
105+
[DataTestMethod]
106+
[DataRow(13608, DisplayName = "JSON validation error code 13608 maps to 400")]
107+
[DataRow(13609, DisplayName = "JSON validation error code 13609 maps to 400")]
108+
[DataRow(13610, DisplayName = "JSON validation error code 13610 maps to 400")]
109+
[DataRow(13611, DisplayName = "JSON validation error code 13611 maps to 400")]
110+
[DataRow(13612, DisplayName = "JSON validation error code 13612 maps to 400")]
111+
[DataRow(13613, DisplayName = "JSON validation error code 13613 maps to 400")]
112+
[DataRow(13614, DisplayName = "JSON validation error code 13614 maps to 400")]
113+
public void TestJsonValidationErrorsMapToBadRequest(int number)
114+
{
115+
RuntimeConfig mockConfig = new(
116+
Schema: "",
117+
DataSource: new(DatabaseType.MSSQL, "", new()),
118+
Runtime: new(
119+
Rest: new(),
120+
GraphQL: new(),
121+
Mcp: new(),
122+
Host: new(null, null, HostMode.Development)
123+
),
124+
Entities: new(new Dictionary<string, Entity>())
125+
);
126+
MockFileSystem fileSystem = new();
127+
fileSystem.AddFile(FileSystemRuntimeConfigLoader.DEFAULT_CONFIG_FILE_NAME, new MockFileData(mockConfig.ToJson()));
128+
FileSystemRuntimeConfigLoader loader = new(fileSystem);
129+
RuntimeConfigProvider provider = new(loader);
130+
DbExceptionParser dbExceptionParser = new MsSqlDbExceptionParser(provider);
131+
132+
DbException sqlException = SqlTestHelper.CreateSqlException(number);
133+
134+
Assert.AreEqual(
135+
System.Net.HttpStatusCode.BadRequest,
136+
dbExceptionParser.GetHttpStatusCodeForException(sqlException));
137+
Assert.AreEqual(
138+
DataApiBuilderException.SubStatusCodes.DatabaseInputError,
139+
dbExceptionParser.GetResultSubStatusCodeForException(sqlException));
140+
}
97141
}
98142
}

0 commit comments

Comments
 (0)