JSON support across providers
There is no single portable JSON API: PostgreSQL exposes a native
json/jsonbsurface, SQL Server returns a document with a trailingFOR JSONclause and offers JSON-as-text functions, MySQL/MariaDB offer the same JSON-as-text functions over theJSON_EXTRACT/JSON_SETfamily, and the remaining providers reject JSON constructs.
Prerequisites: Querying and projections · Scalar functions · Table-valued functions · Provider overview
Overview
nextorm deliberately has no cross-provider JSON function surface. "Working with JSON" means different things on different providers, and the engine keeps those mechanisms separate instead of pretending they are one feature:
- SQL Server has two independent surfaces.
ForJsonexecutes the query and returns the whole result set as one JSON document (FOR JSON PATH/FOR JSON AUTO), andSqlServerprovides the JSON-as-text scalar functions (json_value,json_query,json_modify,isjson) plus theopenjsontable function. SQL Server 2025 (17.x) additionally has a distinct nativejsoncolumn type, reachable for[JsonColumn]only through an explicit version gate — see Nativejsonon SQL Server 2025+. - PostgreSQL has native
json/jsonbtypes and exposes the onlyjson/jsonbfunction/operator surface (SqlFunctions.Postgres): construction, aggregation, access, containment and JSONPath. - MySQL and MariaDB expose the same JSON-as-text surface as SQL Server
(
SupportsTextJson) over theJSON_EXTRACT/JSON_UNQUOTE/JSON_SETfamily. Their JSON columns are not the same thing: MySQL 8 has a realjsontype (the server validates, normalises and reports it asjson), while MariaDB'sJSONis an alias forLONGTEXTplus ajson_valid()CHECK (reported aslongtext). Both are used through the text transport — a[JsonColumn]property serializes to text that the server parses — because neither provider has a bindable native JSON representation, soJsonColumnStorage.Nativeis refused there (see Map a CLR object to a native JSON column). - ClickHouse maps its string-JSON extractors (
JSONExtractString,JSONExtractInt,JSONExtractFloat,JSONExtractBool,JSONExtractRaw,JSONHas,JSONLength,JSONType) — plus the array-returningJSONExtractKeys/JSONExtractArrayRaw(projecting asstring[]) andJSONExtractKeysAndValues(projecting asTuple<string, T>[]) — and the flat-JSON fast path (visitParamExtractString/Int/Float/Bool/Raw) throughSupportsJsonExtract, its JSONPath scalarsJSON_VALUE/JSON_QUERY/JSON_EXISTS(written asjson_value/json_query/json_exists) and its native-JSON functionsJSONAllPaths/JSONAllPathsWithTypes/toJSONString(written asjson_all_paths/json_all_paths_with_types/to_json_string) through the same flag. These take a nativeJSONvalue (CAST(col AS JSON)for aStringcolumn);JSONAllPathsprojects asstring[]andJSONAllPathsWithTypesasMap(String, String)→Dictionary<string, string>. ClickHouse also has a nativeJSONcolumn type (object-rooted): a[JsonColumn]property and a bareJsonObject/JsonDocument/JsonElementprojection/parameter map to it (see Map a CLR object to a native JSON column). - SQLite exposes the JSON1 surface through the provider-specific
SqlFunctions.Sqlite(json/jsonb/json_extract, the->/->>operators,json_set/json_patch, and thejson_each/json_treetable functions). The cross-provider JSON API is not mapped to it: the nativejson/jsonbtype and the text-JSON names (json_value/json_query/json_modify/isjson) throwNotSupportedException, so use the JSON1 members directly (see the SQLite provider). - Every SQL provider shares one JSON terminal:
WriteJson/WriteJsonAsyncstream the query's projection to a caller-ownedStreamas a JSON array or NDJSON, serialized client-side withSystem.Text.Json(see Streaming data to a Stream). On SQL Server an eligible flat projection is instead served by the database through a trailingFOR JSON PATH, whose single document column is copied to the stream in bounded chunks — an internal optimization with the same output contract, not a change to the scalarForJsonbelow (see SQL Server native fast-path). Preflight validation uses a single exception contract —NotSupportedExceptionwith the message conventionJSON streaming validation [<token>]: <context>.and the fixed tokensmode-options,projection,names,unsupported-column,reader-binding,in-memoryandunsupported-execution-form— and an incompatible reader schema is rejected before any output, framing included. This is a serialization terminal, not a SQL JSON function: it does not change the per-provider JSON surfaces below.
Because the mechanisms are different, the same conceptual result is written in different ways. Read the section for your provider; the provider matrix and choosing an approach show the side-by-side equivalents.
Provider matrix
| Provider | ForJson output |
Native json/jsonb |
JSON-as-text functions | openjson as FROM |
|---|---|---|---|---|
| SQL Server | Supported (SupportsForJson) |
json on 2025+ (SupportsJson), opt-in through a declared major version 17+ or an Azure/Fabric SQL database engine edition; object/array roots only. Otherwise JSON lives in nvarchar |
Supported (SupportsTextJson) |
Supported |
| PostgreSQL | No | Supported (SupportsJson) |
No | No |
| SQLite | No | No | No (JSON1 via SqlFunctions.Sqlite) |
No |
| MySQL / MariaDB | No | Real json column (MySQL) / JSON = LONGTEXT + json_valid() CHECK alias (MariaDB), both used through text storage; Native refused |
Supported (SupportsTextJson) |
No |
| ClickHouse | No | Native JSON (SupportsJson; object-rooted, via [JsonColumn]/JsonObject/JsonDocument/JsonElement) |
String JSON + JSONPath scalars + native-JSON functions (JSONAllPaths/JSONAllPathsWithTypes/toJSONString; SupportsJsonExtract) |
No |
| In-memory | Not applicable (no SQL) | Not applicable | Not applicable | Not applicable |
"No" means the command is rejected with NotSupportedException when its SQL is built, not that the
database lacks JSON support.
SQL Server
Return the whole result set as one JSON document
ForJson is a terminal operator: it executes the query and returns the whole
result set as one JSON document (the projection drives the document shape; the query element type is
irrelevant because the database returns a single document column):
string? json = dataContext.From<IComplexEntity>()
.Select(e => new { e.Id, e.String })
.ForJson(ForJsonMode.Path, root: "items", includeNullValues: true);
select id, somestring
from complex_entity for json path, root('items'), include_null_values
Path shapes the document from the projection aliases (the default) and
Auto from the table structure:
string? json = dataContext.From<IComplexEntity>()
.Select(e => new { e.Id, e.String })
.ForJson(ForJsonMode.Auto);
select id, somestring
from complex_entity for json auto
The optional root wraps the document in ROOT('name') and includeNullValues adds
INCLUDE_NULL_VALUES. ForJson returns null when the query produces no rows (SQL Server returns SQL
NULL for an empty FOR JSON result). The same server-side FOR JSON PATH rendering also backs the
WriteJson native fast-path for an eligible flat projection; that path is an internal optimization and
does not change ForJson itself (see
SQL Server native fast-path). The clause is placed after ORDER BY and before a trailing
OPTION (...), so it composes with Hint (apply the hint first, then the
terminal): a for json path option (recompile) query is valid. Use
WithForJson to only attach the clause and keep the command composable. (Table hints,
WithTableHint, attach to the FROM table and are independent of the
JSON clause.) A dialect that does not support the clause rejects the command, and combining ForJson
with ForXml throws NotSupportedException("FOR JSON and FOR XML cannot be combined.").
Read JSON stored in a text column
SQL Server before 2025 and MySQL/MariaDB expose no bindable native JSON representation here: JSON is
stored in an ordinary text column and manipulated with text-oriented functions
(SupportsTextJson). The path is a JSONPath string
('$.name'); json_value returns a scalar, json_query an object/array fragment and json_modify a
modified copy. On MySQL/MariaDB the same expressions render as json_unquote(json_extract(...)),
json_extract(...) and json_set(...). On SQL Server 2025 the same functions keep working over both an
nvarchar column and the native json type (see
Native json on SQL Server 2025+):
var rows = dataContext.From<IComplexEntity>()
.Select(e => new
{
Id = SqlFunctions.SqlServer.json_value(e.String, "$.id"),
Name = SqlFunctions.SqlServer.json_query(e.String, "$.name"),
Updated = SqlFunctions.SqlServer.json_modify(e.String, "$.id", "1")
})
.ToList();
select json_value(somestring, '$.id') as [Id], json_query(somestring, '$.name') as [Name], json_modify(somestring, '$.id', '1') as [Updated]
from complex_entity
SqlFunctions.SqlServer.isjson tests whether a text value is valid JSON. On SQL Server a predicate
renders (isjson(x)) = 1 because T-SQL ISJSON returns an int, and as a projected value it is cast
to bit; on MySQL/MariaDB it renders json_valid(x):
var rows = dataContext.From<IComplexEntity>()
.Where(e => SqlFunctions.SqlServer.isjson(e.String))
.Select(e => new { e.Id })
.ToList();
select id
from complex_entity where (isjson(somestring)) = 1
Native json on SQL Server 2025+
SQL Server 2025 (17.x) has a distinct built-in json type — not an alias of nvarchar — and the
Azure SQL engines built on the same release have it too. nextorm reaches it through the same
[JsonColumn] mapping, gated on a declared state: a context built with Version(17, 0) or higher,
or with an explicit SqlServerEngineEdition of SqlDatabase (5), AzureSqlManagedInstance (8) or
MicrosoftFabricSqlDatabase (12). The default, unversioned construction stays text-only, so an existing
nvarchar-backed mapping never changes under you:
// Azure SQL Database / Fabric SQL database: declare the engine edition, not a fake major version.
using var azure = new SqlServerDataContext(SqlServerEngineEdition.SqlDatabase, connectionString);
// Version-first constructor: the declared major version opens the version-gated capabilities.
using var dataContext = new SqlServerDataContext(new Version(17, 0), connectionString);
[JsonColumn(Storage = JsonColumnStorage.Native)] // Auto and Text are equally valid here
public Address Address { get; set; } = new();
The representation stays the ordinary CLR string: parameters are inferred as nvarchar,
there is no DOM parameter, no SqlDbType.Json and no double serialisation. On a gate-open context
Auto, Text and Native resolve to the same text converter, so the stored bytes are identical to the
text storage — the column type, not the converter, is what differs. What the native column adds is a
server-side constraint: it stores object and array documents only, and rejects the scalar roots
(string, number, boolean) and the JSON null document with server error 13609 — use the text
storage for those payloads. That rejection is the server's, raised at execution; nextorm adds no
client-side rule of its own.
Edition 11 ("Azure Synapse serverless SQL pool, or Microsoft Fabric") does not grant the capability
by edition; only 5, 8 and 12 do. nextorm never probes SERVERPROPERTY('EngineEdition') — the edition is
a declaration, immutable per context type, and SQL generation depends only on the declared state. Azure
SQL Database, Managed Instance and Fabric SQL database are unit-tested only: no live Azure/Fabric
instance was available for this validation, and the claim does not extend to every Fabric workload.
nextorm also generates no column DDL, so switching an existing column to json stays a
deployment/migration decision.
Turn JSON into rows with openjson
SqlFunctions.SqlServer.openjson(json) is a table-valued function (SQL Server 2016+) used through
From with a factory call. Its default
schema yields the properties of a JSON object or the elements of a JSON array as
SqlFunctions.IOpenJsonRow (Key/Value/Type):
var json = """{"a":1,"b":2}""";
var entries = dataContext
.From(() => SqlFunctions.SqlServer.openjson(json))
.Select(r => new { r.Key, r.Value, r.Type })
.ToList();
select [key] as [Key], value, type
from openjson(@json) as [t1]
For a typed projection declare your own TvfEntityBuilder<T> factory whose row shape matches the
WITH (...) clause and pass the clause body as withClause; nextorm only emits the call, it does not
create the function.
SqlFunctions.SqlServer.string_split follows the same pattern for a comma-separated string. Both are
gated by SupportsTableFunction, so only SQL Server emits them.
PostgreSQL
Pass a JSON value as a parameter
A parameter whose runtime value is a JsonDocument, JsonElement or JsonNode is bound as jsonb, so
it can be used directly with the JSON operators:
using System.Text.Json;
var document = JsonDocument.Parse("""{"name":"Alice","tags":["a","b"]}""");
var rows = dataContext.From<IComplexEntity>()
.Where(e => SqlFunctions.Postgres.json_get_text(SqlFunctions.Parameter<JsonDocument>(0), "name") == "Alice")
.Select(e => new { e.Id })
.ToList(document);
select id
from complex_entity where ((@norm_p0 ->> 'name') = 'Alice')
Provider parameter binding: a plain JSON string parameter is bound as text. The six
accessors json_get, json_get_text, json_get_path and json_get_path_text (both the string-key and
the integer-index overloads) are the exception — they apply contextual SQL typing: a raw client
document value (a literal, a captured value or an explicit parameter) is parsed as jsonb in the generated
SQL, and the right operand is typed per overload (text for a string key, integer for an index, text[]
for a path). Native json/jsonb columns and server-side JSON expressions are not cast and keep their
operand and result types. For every other JSON function, parse a plain string explicitly with
SqlFunctions.Postgres.json_cast(value) (cast(value as jsonb)).
Map a CLR object to a native JSON column
A [JsonColumn] property stores any serializable CLR object in the column as JSON. The default
JsonColumnStorage.Auto uses the provider's native type where it has one, so on PostgreSQL the property
maps to a native jsonb column for both read and write — insert, update and RETURNING — while Native
forces jsonb and Text forces a text column. The property participates in projections and constant
comparisons like any other converted column:
[SqlTable("customers")]
public sealed class Customer
{
public long Id { get; set; }
[JsonColumn]
public Address Address { get; set; } = new();
}
The mapping covers the property, not the schema: nextorm does not generate the column DDL, so the table
must already have the jsonb column (the same holds for SQL Server's native json column — see
Native json on SQL Server 2025+). See
Value converters for the fluent
JsonColumn(o => ...) form and serializer options.
On MySQL and MariaDB the same attribute keeps the text storage, because those providers have a
JSON column but no bindable native JSON representation (MySqlConnector rejects JsonElement,
JsonDocument and JsonNode parameters, MySqlDbType.JSON accepts strings only, and a json column
always reads back as string). This is fully supported storage, not a refusal of JSON columns: a
MySQL 8 table may declare the column as json — the server validates and normalises the document — and a
MariaDB table may declare it as JSON, which the server reports as longtext with a json_valid()
CHECK. An explicit JsonColumnStorage.Native on either engine fails closed with a
NotSupportedException naming the dialect (MySqlDialect/MariaDbDialect) before any command runs;
use Auto (text) or Text. Note also that MySQL compares a json document as a typed value — a string
operand does not equal it — and nextorm promises no equality translation for that column.
Bare CLR JSON values are handled at the boundary, differently from a mapped column:
- A
JsonDocumentorJsonElementvalue materializes from ajson/jsonbcolumn in a named projection, and a parameter of either type binds asjsonb. - A bare
JsonNode/JsonNode?property (declared exactlyJsonNode, notJsonObject/JsonArray) reads a nativejson/jsonbcolumn: the text is read and parsed, covering object, array and scalar roots, in both scalar (Select(x => x.Data)) and composite projections. A SQLNULLand a JSON literalnullboth become CLRnull. DeclaredJsonObject/JsonArrayproperties, and a bareJsonDocument/JsonElementscalar projection, are outside this support — map those with[JsonColumn]or project them in a named shape. AJsonNodeparameter binds asjsonb. - A plain
stringmaps totext. The six accessors above (json_get,json_get_text,json_get_path,json_get_path_text, string-key and integer-index overloads) accept a raw client document string — a literal, a captured value or an explicit parameter — because they apply contextual SQL typing and parse it asjsonbin the generated SQL, typing the key/index/path astext/integer/text[]. A nativejson/jsonbcolumn or a server-side JSON expression is left uncast, so its operand and result types do not change. For any other JSON function, useSqlFunctions.Postgres.json_cast(value)to parse a string.
On ClickHouse the same section header applies to the native JSON column, whose physical type is
JSON and which is object-rooted: it accepts JSON objects at the root and rejects arrays and
scalar roots. A [JsonColumn] property with Auto or Native storage maps an object-root POCO,
JsonObject, JsonDocument or JsonElement to the native JSON type for both read and write, while
Text keeps a String column; a model that does not serialize to an object fails closed with
NotSupportedException. The same object-root DOM values map as bare projections and parameters. The
projection must go through a named shape — Select(x => new { x.Doc }) or a DTO — because a bare
top-level scalar Select(x => x.Doc) is not handled by the core projection classifier. An empty
object {} stays a non-null value, while SQL NULL in a Nullable(JSON) column stays distinct
from it (null for JsonObject/JsonDocument, default/JsonValueKind.Undefined for the value-type
JsonElement). A bare string read over a JSON column is not mapped. The legacy Object('json')
alias is not supported by this integration: with ClickHouse.Driver 1.4.0.0 and ClickHouse
25.8.33.6 a connection-level allow_experimental_object_type=1 lets the type be created, but reads
materialize as Tuple<SByte,String> (path → value) and cannot be read through the native JsonObject
transport (GetFieldValue<JsonObject> throws InvalidCastException). nextorm does not enable
allow_experimental_object_type itself.
Build a JSON object or array in the projection
Construction functions build a json/jsonb value from ordinary SQL expressions. String arguments that
are constants become the keys:
var document = dataContext.From<IComplexEntity>()
.Select(e => new { V = SqlFunctions.Postgres.jsonb_build_object("id", e.Id, "name", e.String) })
.First()
.V;
select jsonb_build_object('id', id, 'name', somestring) as "V"
from complex_entity
| C# | SQL |
|---|---|
SqlFunctions.Postgres.jsonb_build_object("a", x, ...) |
jsonb_build_object('a', x, ...) |
SqlFunctions.Postgres.jsonb_build_array(x, y) |
jsonb_build_array(x, y) |
SqlFunctions.Postgres.json_array(x, y) / jsonb_array(x, y) |
json_array(x, y) / json_array(x, y returning jsonb) |
SqlFunctions.Postgres.to_jsonb(x) |
to_jsonb(x) |
Collapse a result set into one document (the FOR JSON analogue)
PostgreSQL has no FOR JSON suffix; the equivalent is the jsonb_agg aggregate over a projection. Use
jsonb_agg/json_agg for an array of values or jsonb_object_agg for a key/value map:
var json = dataContext.From<IComplexEntity>()
.Select(e => SqlFunctions.Postgres.jsonb_agg(
SqlFunctions.Postgres.jsonb_build_object("id", e.Id, "name", e.String)))
.First();
select jsonb_agg(jsonb_build_object('id', id, 'name', somestring))
from complex_entity
Nesting is expressed with GroupBy: the outer aggregate makes the document, the inner one makes each
group's array:
var groups = dataContext.From<IComplexEntity>()
.GroupBy(e => new { e.Int })
.Select(e => new
{
e.Int,
Items = SqlFunctions.Postgres.jsonb_agg(
SqlFunctions.Postgres.jsonb_build_object("id", e.Id, "name", e.String))
})
.ToList();
-- jsonb_agg(jsonb_build_object('id', id, 'name', somestring)) grouped by int
json_object_agg(key, value) / jsonb_object_agg(key, value) produce a JSON object instead of an array
(jsonb_object_agg(id, somestring)).
Read and filter JSON
The access operators and predicates are exposed as SqlFunctions.Postgres methods:
| C# | SQL |
|---|---|
SqlFunctions.Postgres.json_get(json, "key") / json_get(json, 0) |
json -> key / json -> 0 |
SqlFunctions.Postgres.json_get_text(json, "key") / json_get_text(json, 0) |
json ->> key / json ->> 0 |
SqlFunctions.Postgres.json_get_path(json, path) / json_get_path_text(json, path) |
json #> path / json #>> path |
SqlFunctions.Postgres.json_contains(a, b) |
a @> b |
SqlFunctions.Postgres.json_exists(json, "key") |
json ? 'key' |
SqlFunctions.Postgres.json_exists_any(json, keys) / json_exists_all(json, keys) |
json ?\| keys / json ?& keys |
SqlFunctions.Postgres.jsonb_path_exists(json, path) / jsonb_path_match(json, path) |
jsonb_path_exists(json, cast(path as jsonpath)) / jsonb_path_match(...) |
SqlFunctions.Postgres.jsonb_path_query_first(json, path) / jsonb_path_query_array(json, path) |
jsonb_path_query_first(...) / jsonb_path_query_array(...) |
The four accessors above apply contextual SQL typing: a raw client document value (a literal, a
captured value or an explicit parameter) is parsed as jsonb, so json_get("{\"a\":1}", "a") renders
cast('{"a":1}' as jsonb) -> cast('a' as text), while a native json/jsonb column or a server-side JSON
expression is emitted unchanged (doc -> cast('a' as text)), preserving its operand and result types. The
key/index/path is always typed for its overload: cast(<key> as text), cast(<index> as integer),
cast(<path> as text[]) — a literal key is inlined as cast('a' as text). An ordinary string parameter
is bound as text.
A path/keys operand is a string[] bound as a single array parameter, so
json_get_path(json, new[] { "a", "b" }) renders json #> cast(@p0 as text[]). JSONPath functions take
the path as a plain string and cast it to jsonpath; JSON_EXISTS, jsonpath and the sibling JSON
functions keep their own independent behavior.
var path = new[] { "a", "b" };
var rows = dataContext.From<IComplexEntity>()
.Where(e => SqlFunctions.Postgres.json_contains(
SqlFunctions.Parameter<JsonDocument>(0),
SqlFunctions.Parameter<JsonDocument>(1)))
.Where(e => SqlFunctions.Postgres.json_exists(SqlFunctions.Parameter<JsonDocument>(2), "key"))
.Select(e => new { e.Id })
.ToList();
select id
from complex_entity where (@norm_p0 @> @norm_p1) and (@norm_p2 ? 'key')
SQL/JSON query functions
The SQL/JSON query functions take a jsonpath (cast from the path string) and select a value rather
than a key:
| C# | SQL |
|---|---|
SqlFunctions.Postgres.json_value(json, path) |
json_value(json, cast(path as jsonpath)) (returns text) |
SqlFunctions.Postgres.json_query(json, path) |
json_query(json, cast(path as jsonpath)) (returns jsonb) |
SqlFunctions.Postgres.json_exists(json, path, fromJsonPath) |
json_exists(json, cast(path as jsonpath)) (returns boolean) |
var json = SqlFunctions.Postgres.jsonb_build_object("a", 1);
var rows = dataContext.From<IComplexEntity>()
.Select(e => new
{
Value = SqlFunctions.Postgres.json_value(json, "$.a"),
Exists = SqlFunctions.Postgres.json_exists(json, "$.a", true)
})
.ToList();
select json_value(jsonb_build_object('a', 1), cast('$.a' as jsonpath)) as "Value",
json_exists(jsonb_build_object('a', 1), cast('$.a' as jsonpath)) as "Exists"
from complex_entity
json_exists has two forms: the two-argument json_exists(json, "key") is the top-level ? operator,
while the three-argument json_exists(json, path, fromJsonPath) is the SQL/JSON path function (the
third argument is a discriminator and is not rendered into SQL).
The construction and aggregation functions return string?; deserialize with
JsonSerializer.Deserialize<T>(...) when you need a .NET object.
Choosing an approach
| Goal | SQL Server | PostgreSQL |
|---|---|---|
| One JSON document for the whole result set | .ForJson(...) |
jsonb_agg(jsonb_build_object(...)) |
| One JSON object per row | json_query/text concatenation, or assemble on the client |
jsonb_build_object(...) |
| Read a field from a JSON value | json_value(col, '$.x') |
col ->> 'x' |
| Test that a value is JSON | isjson(col) |
the value is typed json/jsonb by the column |
| Filter by JSON content | json_value(col, '$.x') = ... |
col @> ..., col ? 'x', col #> ... |
| Expand JSON into rows | openjson(...) as a FROM source |
no built-in helper; declare a TvfEntityBuilder<T> factory |
Limitations
- There is no portable JSON abstraction. Code written for one provider's JSON surface throws
NotSupportedExceptionon another; useISqlDialectcapability flags if you must branch. - The JSON streaming terminal validates its options and projection up front and fails with
NotSupportedExceptionunder the stableJSON streaming validation [<token>]: <context>.convention (tokensmode-options,projection,names,unsupported-column,reader-binding,in-memory,unsupported-execution-form); an incompatible reader schema is rejected before any output, the array/root framing included. Lifecycle failures stayInvalidOperationException/ObjectDisposedException, while destination/provider errors and cancellation keep their category. See Validation exceptions and runtime errors. ForJson/ForXmlare SQL Server only (SupportsForJson/SupportsForXml), and the two are mutually exclusive on one command.- The PostgreSQL
json/jsonbsurface (SqlFunctions.Postgres) requiresSupportsPostgresJsonSql(PostgreSQL only); the SQL Server and MySQL/MariaDB text functions requireSupportsTextJson. Each throws on the other provider. - ClickHouse's JSON surface (
JSONExtract*/visitParamExtract*, the JSONPath scalars and the native-JSONJSONAllPaths/JSONAllPathsWithTypes/toJSONString) requiresSupportsJsonExtract; it does not overlap with theSupportsTextJsonset. The native-JSON functions take a nativeJSONvalue (cast aStringcolumn withCAST(col AS JSON));json_all_pathsprojects asstring[]andjson_all_paths_with_typesasDictionary<string, string>(mapKeys/mapValuesturn the map into a collection). A nativeJSONcolumn stores nativeJSONfor[JsonColumn](Auto/Native, object-root POCO/JsonObject/JsonDocument/JsonElement) and for bare object-rootJsonObject/JsonDocument/JsonElementprojections/parameters (see Map a CLR object to a native JSON column). Arrays/scalar roots, a barestring, and the legacyObject('json')alias are not supported, and a bare top-level scalarSelect(x => x.Doc)is not handled by the core projection classifier. - SQLite's JSON1 extension is exposed only through the provider-specific
SqlFunctions.Sqlitesurface (json_extract,->/->>,json_each/json_tree, ...); the cross-provider native/json_*API is not mapped to it, so those names throwNotSupportedException. - The in-memory provider produces no SQL, so
ForJson/ForXmlthrowNotSupportedExceptionand the JSON function surfaces do not apply to it. - PostgreSQL maps a
[JsonColumn]property to a nativejsonbcolumn (Autostorage) on read and write (insert, update andRETURNING), and aJsonDocument/JsonElement/JsonNodeparameter binds asjsonb. ClickHouse maps[JsonColumn]Auto/Nativeand bare object-rootJsonObject/JsonDocument/JsonElementto its native object-rootedJSONcolumn. SQL Server maps[JsonColumn]Auto/Nativeto its nativejsontype only on a context that declares major version 17 or higher, or an Azure/Fabric SQL database engine edition, and then only for object/array documents — a scalar root or the JSONnulldocument is refused by the server with error 13609, so those payloads needTextstorage; on a pre-2025 context with no version-17-equivalent edition declaration SQL Server JSON is always text. MySQL/MariaDB JSON columns are reached through the text transport on both engines (Auto/Text), andNativeis refused with the dialect-namedNotSupportedException.
See also
- Querying and projections -
ForJson/ForXmlfor SQL Server. - Scalar functions - the full PostgreSQL JSON/JSONB surface and the SQL Server / MySQL/MariaDB text-JSON functions.
- Table-valued functions -
openjsonandstring_split. - PostgreSQL provider - JSON columns, parameter binding and arrays.
- SQL Server provider -
FOR JSON,FOR XMLand text JSON. - MySQL provider and MariaDB provider - text JSON over
JSON_EXTRACT/JSON_SET. - Limitations and out-of-scope features
Source: src/nextorm.core/Visitors/Structured/JsonSqlTranslator.cs, src/nextorm.core/Query/SqlFunctions/SqlFunctions.Postgres.cs,
src/nextorm.core/Query/SqlFunctions/SqlFunctions.SqlServer.cs, src/nextorm.core/Expressions/Formats/ForJson.cs,
src/nextorm.core/DataContext/Dialect/ISqlDialect.cs, src/nextorm.sqlserver/SqlServerDialect.cs,
src/nextorm.postgres/PostgresDialect.cs; tests tests/nextorm.sqlserver.tests/SqlGeneration/SqlGenerationTests.cs:443,695,1313,1390,
tests/nextorm.postgres.tests/SqlGeneration/SqlGenerationTests.cs:848,868,887,930,950,1831,1861,
tests/nextorm.sqlite.tests/SqlGeneration/SqlGenerationTests.cs:1773.