Concatenate or merge two json objects in SQL Server
Asked Answered
A

9

11

I have a table storing json in one column. I would like to update the json value by merging in another json.

Something like:

insert into mytable 
values ('{ "a": "b" ')

update mytable 
set jsonColumn = JSON_MERGE(jsonColumn, '{ "c": 2 }')

This should result in json like this:

{ "a": "b", "c": 2 }

Unfortunately there is no such JSON_MERGE function and JSON_MODIFY lets me modify only columns one by one. I have too many of them including nested properties.

I'm basically searching for an equivalent to postgres || concatenation operator.

Arose answered 21/2, 2018 at 17:2 Comment(2)
Jan, not the answer you're looking for, but you can create a user function called JSON_MERGE that does this. The functions would accept the existing value from the column, and a varchar(MAX) of the value to add. You can look for "{" and "}" and use the built-in SQL REPLACE function. I can see where more complex JSON would get more complicated, however. Can the JSON_MODIFY function not be applied to a set in an UPDATE statement?Suk
String manipulation gets likely too messy when working with complex nested structures. I was not able to make JSON_MODIFY working on set in update.Arose
D
6

In Sql Server 2016 it's not possible to use variables as json path in JSON_MODIFY, so I'm not sure if there's an elegant solution for this problem.

If you have Sql Server 2017, then it seems to be possible.

create function dbo.fn_json_merge
(
    @a nvarchar(max),
    @b nvarchar(max)
)
returns nvarchar(max)
as
begin
    if left(@a, 1) = '{' and left(@b, 1) = '{' begin
        select
            @a = case when d.[type] in (4,5) then json_modify(@a, concat('$.',d.[key]), json_query(d.[value])) else @a end,
            @a = case when d.[type] not in (4,5) then json_modify(@a, concat('$.',d.[key]), d.[value]) else @a end
        from openjson(@b) as d;
    end else if left(@a, 1) = '[' and left(@b, 1) = '{' begin
        select @a = json_modify(@a, 'append $', json_query(@b));
    end else begin
        select @a = concat('[', @a, ',', right(@b, len(@b) - 1));
    end;

    return @a;
end;

Couple of notes:

  • For the sake of simplicity I didn't add checking that both objects are actually valid json;
  • I don't know if there's a better way to check that given string is json array or json object;
  • It's not possible to add first element of array with json_modify so there's a fallback to simple CONCAT in case first string is an object and second is an array;
  • I had to creatively use JSON_QUERY function so jsons will be inserted correctly;
  • I've used the fact that if you assign the variable in SELECT statement then you can use previous value of the variable in the assignment statement;

sql server fiddle demo

postgresql fiddle example

update I've added a bit improved version which should work with different types of values better:

create function dbo.fn_json_merge
(
    @a nvarchar(max),
    @b nvarchar(max)
)
returns nvarchar(max)
as
begin
    if left(@a, 1) = '{' and left(@b, 1) = '{' begin
        select @a =
            case
                when d.[type] in (4,5) then
                    json_modify(@a, concat('$.',d.[key]), json_query(d.[value]))
                when d.[type] in (3) then
                    json_modify(@a, concat('$.',d.[key]), cast(d.[value] as bit))
                when d.[type] in (2) and try_cast(d.[value] as int) = 1 then
                    json_modify(@a, concat('$.',d.[key]), cast(d.[value] as int))
                when d.[type] in (0) then
                    json_modify(json_modify(@a, concat('lax $.',d.[key]), 'null'), concat('strict $.',d.[key]), null)
                else
                    json_modify(@a, concat('$.',d.[key]), d.[value])
            end
        from openjson(@b) as d
    end else if left(@a, 1) = '[' and left(@b, 1) = '{' begin
        select @a = json_modify(@a, 'append $', json_query(@b))
    end else begin
        select @a = concat('[', @a, ',', right(@b, len(@b) - 1))
    end

    return @a
end

sql fiddle demo

Dickinson answered 5/4, 2019 at 15:35 Comment(3)
Just a small improvement: you could quote the keys concat('$."',d.[key],'"'), so that key strings that also happen to be numbers work (e.g. { "0": "abc" }). There's also a problem when the numerical is not an integer: it would be turned into a string here. I think a good workaround would be to cast to a decimal with the right precision detected according to the string (e.g. 1.23 cast as decimal(3,2)), when type is 2. (Not sure why they called the type obtained from openjson "int", when it can be a decimal number in JSON. JSON support in SQL Server still needs some improvement.)Otha
@Otha +1 for the suggestion to add quotes to the key. It is needed to handle special characters or numbers in the key.Outpour
The final version with all improvements:Madancy
M
4

I have a solution for your issue. I found it while trying to merge 2 different JSON objects and I used JSON_MODIFY and OPENJSON functions.

Sample data:

JSON1: {"a": 1, "b": 2, "c": 3}
JSON2: {"d": 4, "e": 5}

Here the solution

DECLARE @vJSON NVARCHAR(MAX) = N'{"a":1, "b":2, "c":3}'
DECLARE @vJSON2 NVARCHAR(MAX) = N'{"d":4, "e":5}'

SELECT
    @vJSON = JSON_MODIFY(@vJSON, CONCAT(N'$.', [Key]), value)
FROM
OPENJSON(@vJSON2)

SELECT @vJSON
--Output: {"a":1, "b":2, "c":3, "d":"4", "e":"5"}

Also it's not a type-safe solution, you can add a case statement to casting values according to type of OPENJSON

Metronymic answered 7/12, 2022 at 8:34 Comment(0)
H
3

Also a bit late to the party, but we faced similar issues trying to merge JSONs in MS SQL. We also wanted it to be recursive and allow us to define strategy for arrays like "union", "concat" and "replace".

Our solution for JSON manipulations like merge, JSON path expressions and more just turned into open source and is now available @ Github

Feel free to use, comment and contribute so we can further improve JSON methods for MS SQL.

Hypopituitarism answered 30/10, 2019 at 17:41 Comment(0)
W
2

You can do something similar to that code:

DECLARE @json1 nvarchar(max),
        @json2 nvarchar(max)

DECLARE @result AS nvarchar(max)

SET @json1 = N'{"a": "1", "c": "3"}'

SET @json2 = N'{"b": "2"}'

SELECT
  @result = COALESCE(@result + ', ', '') + '"' + [key] + '":"' + value + '"'
FROM (SELECT
  [key],
  value
FROM OPENJSON(@json1)
UNION ALL
SELECT
  [key],
  value
FROM OPENJSON(@json2)) AS x

SET @result = '{' + @result + '}'

PRINT @result

the @result is

{"a":"1", "c":"3", "b":"2"}
Whoa answered 22/2, 2018 at 11:24 Comment(3)
Thank you. Unfortunately this doesn't work with nested structures.Like jsonA {"a": { "a": "a" } } and jsonB {"a": { "b": "b" } } also I was hoping for more elegant solution.Arose
What you expect as output in that case?Whoa
The same what js Object.assign would do. So {"a": { "b": "b"} }Arose
H
0

There is concept of append in JSON_MODIFY but that is dependent on the tag name. Please see the example below. if you have tag name then it can work otherwise not. Read more from https://learn.microsoft.com/en-us/sql/t-sql/functions/json-modify-transact-sql. Also note that at one point in time you can append a single value

PRINT 'EXAMPLE 1

'

DECLARE @j NVARCHAR(MAX)
SET @j = '{"k" : ["a","b"] }'
PRINT @J
SET @J=JSON_MODIFY(JSON_MODIFY(@j,'append $.k','c'),'append $.k','2')
PRINT @J
GO

PRINT '
EXAMPLE 2

'

DECLARE @j NVARCHAR(MAX)
SET @j = '{"a":"b"}'
PRINT @J
SET @J=JSON_MODIFY(@J,'append $','c:2')
PRINT @J
GO

OUTPUT

EXAMPLE 1

{"k" : ["a","b"] }
{"k" : ["a","b","c","2"] }

EXAMPLE 2

{"a":"b"}
{"a":"b"}
Hagiocracy answered 22/2, 2018 at 5:54 Comment(2)
The append works only with arrays as I see. I need to merge complex json structures.Arose
Ohh.. then you need to do some custom thing.Hagiocracy
B
0

I'm a bit late to the party, but I'm running into something similar at the moment. I made a solution based off of this problem that will merge top-level JSON items.

Some examples of what this would do:

{"a":1} + {"B":2} = {"a":1,"B":2}
{"x":true,"y":{"a":"b","c":"d"}} + {"y":{"a":"z"}} = {"x":true,"y":{"a":"z"}}

This version would not drill down to merge sub-items (for example, it would not keep the ["y"]["c"] index in my second example). I'd imagine that it could be enhanced to do so, but this was a quick proof-of-concept version and I don't need to worry about those kind of updates for my purposes.

Content:

--- Merge the top-level items of two JSON object strings into one JSON
--- based off of: https://mcmap.net/q/1016923/-generate-a-json-string-containing-the-differences-in-two-other-json-strings-using-t-sql

 DECLARE  @jsonA     NVARCHAR(MAX) = '{"CommonValue":"OriginalThing", "OldValue": "A", "ComplexValue": {"InnerValue": "ABC"}}'
        ,@jsonB     NVARCHAR(MAX) = '{"CommonValue":"ChangedThing", "NewValue": "B", "Number": 22}'
        ,@result    NVARCHAR(MAX) = ''

--- Catalog of differences.
DECLARE @JsonDiff TABLE 
(
    OldKey CHAR(128),
    OldValue NVARCHAR(MAX),
    OldType CHAR(1),
    NewKey CHAR(128),
    NewValue NVARCHAR(MAX),
    NewType CHAR(1)
)
--- Temporary table for output rows.
--- The table could probably clipped out for production stuff.
--- For proof-of-concept, it's useful for querying results
---  before building the JSON string.
DECLARE @JsonData TABLE 
(
    NewKey CHAR(128),
    NewValue NVARCHAR(MAX),
    NewType CHAR(1)
)

;WITH DSA AS
(
    SELECT *
    FROM OPENJSON(@jsonA)   
)
,DSB AS
(
    SELECT *
    FROM OPENJSON(@jsonB)   
)
INSERT INTO @JsonDiff (OldKey, OldValue, OldType, NewKey, NewValue, NewType)
SELECT a.[Key] aKey, a.[Value] aValue, a.[Type] aType, b.[Key] bKey, b.[Value] bValue, b.[Type] bType
FROM DSA A
FULL OUTER JOIN DSB B ON A.[key] = B.[key]

INSERT INTO @JsonData (NewKey, NewValue, NewType) 
    SELECT OldKey as k, OldValue as v, OldType as t
    FROM @JsonDiff
    WHERE OldKey IS NOT NULL AND NewKey IS NULL
    UNION
    SELECT NewKey as k, NewValue as v, NewType as t
    FROM @JsonDiff
    WHERE NewKey IS NOT NULL

--- a few queries for display purposes
--- select * FROM @JsonDiff
select NewKey, NewValue FROM @JsonData

SELECT @result += CONCAT ( '"', TRIM([NewKey]), '":'
    ,IIF([NewType] = 1, CONCAT('"', [NewValue], '"'), [NewValue]) -- If the item is a string, then add quotes.
    ,','
)
FROM @JsonData

--- Print the JSON
SELECT CONCAT('{', LEFT(@result, LEN(@result) - 1), '}')

Edit: Here's a slightly more streamlined version of the last bit that removes the need to have @JsonData:

SELECT @result += CONCAT ( '"', TRIM([k]), '":'
    ,IIF([t] = 1, CONCAT('"', [v], '"'), [v]) -- If the item is a string, then add quotes.
    ,','
)
FROM 
    (
        SELECT OldKey as k, OldValue as v, OldType as t
            FROM @JsonDiff
            WHERE OldKey IS NOT NULL AND NewKey IS NULL
        UNION
        SELECT NewKey as k, NewValue as v, NewType as t
            FROM @JsonDiff
            WHERE NewKey IS NOT NULL
    ) as mid

--- Print the JSON
SELECT CONCAT('{', LEFT(@result, LEN(@result) - 1), '}')
Boule answered 5/4, 2019 at 2:3 Comment(0)
I
0

Could also possibly look at:

SELECT (
    SELECT 
    (
        SELECT    ID AS "test.id" 
        FROM      [Table1] 
        FOR       JSON AUTO
    )             AS 'test1', 
                  'id' AS 'test2'
    FROM          test2
    FOR           JSON AUTO
)                 AS JSON
Imbed answered 27/5, 2021 at 1:46 Comment(0)
E
0

Based on @JorgeRibeiro answer , if you want to edit existing value also this will work only for one level json-

DECLARE @json1 nvarchar(max),
        @json2 nvarchar(max)

DECLARE @result AS nvarchar(max)

SET @json1 = N'{"a": "1", "c": "3"}'

SET @json2 = N'{"a": "2","b" : "4"}'

SELECT
  @result = COALESCE(@result + ', ', '') + '"' + [key] + '":"' + value + '"'
FROM (SELECT
  [key],
  value
FROM OPENJSON(@json1) where [key] not in (Select Distinct [key] from OPENJSON(@json2))
UNION ALL
SELECT
  [key],
  value
FROM OPENJSON(@json2)) AS x

SET @result = '{' + @result + '}'

PRINT @result
Ergocalciferol answered 19/8, 2021 at 9:25 Comment(0)
M
0

Just so as not to lose.

The final version of a function from "Roman Pekar" with all improvements:

create function dbo.fn_json_merge ( @a nvarchar(max),
@b nvarchar(max) ) returns nvarchar(max) as
begin
    if left(@a, 1) = '{' and left(@b,1) = '{'
begin
        select
    @a = case
        when d.[type] in (4, 5) then json_modify(@a,concat('$."', d.[key],'"'), json_query(d.[value]))
        when d.[type] in (3) then json_modify(@a, concat('$."', d.[key],'"'),cast(d.[value] as bit))
        when d.[type] in (2) and try_cast(d.[value] as int) IS NOT NULL then json_modify(@a, concat('$."', d.[key],'"'), cast(d.[value] as int))
        when d.[type] in (2) and try_cast(d.[value] as float) IS NOT NULL then json_modify(@a, concat('$."', d.[key],'"'), cast(d.[value] as float))
        when d.[type] in (0) then json_modify(json_modify(@a,concat('lax $."', d.[key],'"'),    'null'),
        concat('strict $."', d.[key],'"'),  null)
        else json_modify(@a,concat('$."', d.[key],'"'),d.[value])
    end
from
    openjson(@b) as d
end
else if left(@a,1) = '[' and left(@b,1) = '{' begin
select
    @a = json_modify(@a,'append $', json_query(@b))
end
else begin
select
    @a = concat('[', @a, ',', right(@b, len(@b) - 1))
end return @a
end;
Madancy answered 7/5 at 13:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.