Json Parameters NET6 - lobodava/artisan-orm GitHub Wiki
AddJsonParam<T> serialises a CLR object to JSON via System.Text.Json and binds it to an nvarchar(max) parameter. The procedure parses it on the SQL side via OPENJSON. It's a pragmatic alternative to user-defined table types when the payload doesn't fit a single rectangular shape.
Available on net8.0 and net10.0 targets only — guarded by #if NET6_0_OR_GREATER.
Added in v4 — see What's New in v4.
public static void AddJsonParam<T>(this SqlCommand cmd, string parameterName, T? value);null values are sent as SQL NULL (not the string "null"). Any non-null T — including anonymous types, records, classes, and primitives — is serialised with the default JsonSerializerOptions.
C# call site:
public void SavePreferences(int userId, UserPrefs prefs)
{
ExecuteCommand(cmd =>
{
cmd.UseProcedure("dbo.UpsertUserPreferences");
cmd.AddIntParam ("@UserId", userId);
cmd.AddJsonParam("@Preferences", prefs);
});
}T-SQL counterpart:
create procedure dbo.UpsertUserPreferences
@UserId int,
@Preferences nvarchar(max)
as
begin
set nocount on;
declare @Theme nvarchar(50),
@FontSize int,
@Locale nvarchar(10);
select @Theme = Theme,
@FontSize = FontSize,
@Locale = Locale
from openjson(@Preferences)
with (
Theme nvarchar(50) '$.theme',
FontSize int '$.fontSize',
Locale nvarchar(10) '$.locale'
);
merge into dbo.UserPreferences as target
using (select @UserId as UserId) as source
on target.UserId = source.UserId
when matched then update set Theme = @Theme, FontSize = @FontSize, Locale = @Locale
when not matched then insert (UserId, Theme, FontSize, Locale)
values (@UserId, @Theme, @FontSize, @Locale);
end;Note the '$.theme' JSON path: System.Text.Json defaults to camelCase property names, so a C# property Theme lands at JSON path $.theme. Match the casing in your OPENJSON ... WITH clause.
Convenient for tightly-scoped payloads that don't deserve a dedicated DTO:
cmd.AddJsonParam("@Filter", new
{
minAmount = 100m,
statuses = new[] { "Active", "Pending" },
fromDate = DateTime.UtcNow.AddDays(-30)
});OPENJSON can also project an array of objects into a tabular result, which is occasionally useful as a lighter-weight alternative to a TVP:
public void BulkUpdateRecords(IList<RecordPatch> patches)
{
ExecuteCommand(cmd =>
{
cmd.UseProcedure("dbo.PatchRecords");
cmd.AddJsonParam("@Patches", patches);
});
}create procedure dbo.PatchRecords
@Patches nvarchar(max)
as
begin
set nocount on;
update r
set r.Name = p.Name,
r.Status = p.Status
from dbo.Records r
inner join openjson(@Patches)
with (
Id int '$.id',
Name nvarchar(100) '$.name',
Status nvarchar(20) '$.status'
) p on p.Id = r.Id;
end;| Aspect | AddJsonParam<T> |
AddTableParam (TVP) |
|---|---|---|
| SQL side parsing cost | One OPENJSON per call — measurable for large payloads. |
Free — the TVP arrives as a typed table. |
| Schema validation | Soft — bad JSON throws at parse time; types coerced. | Strict — column types validated at the boundary. |
| Database setup | None — only the procedure parameter is nvarchar(max). |
Requires creating a user-defined table type beforehand. |
| Heterogeneous payloads | Natural — nested objects, arrays, optional fields. | Awkward — every TVP is a flat rectangle. |
| Maximum size |
nvarchar(max) — ~2 GB. |
TVP rows — millions of rows possible, faster on bulk inserts. |
| Suitable for batch insert | OK for small batches (hundreds). | Better for large batches (thousands+). |
Rule of thumb:
- Single complex object (settings, filter, configuration) → JSON.
- Many flat rows of the same shape → TVP.
- Many nested objects → either work; pick whichever your team already has tooling for.
System.Text.Json is locked in for now — there is no overload that accepts a JsonSerializerOptions. If you need custom serialisation (snake_case, custom converters, etc.), serialise manually first and pass the resulting string via cmd.AddNVarcharMaxParam("@Json", json).
To go the other direction — read a JSON column produced by FOR JSON — read it as a regular string:
var json = cmd.ReadTo<string>("select OrderJson from dbo.Orders where Id = @Id");
var order = JsonSerializer.Deserialize<Order>(json);There is no built-in ReadJson<T> helper because deserialisation rarely benefits from being inside the data-access layer.
See also:
- User-Defined Table Types — the alternative for tabular payloads
- What's New in v4
OPENJSON(T-SQL)