0

I have JSON like this:

{
  "_key": {
    "id": "3b8d1ed7-c3ec-32ec-a083-2ddc17152e94",
    "rootId": "15c85327-9628-3685-b84a-375b546ba92a",
  },
  "employeeInfo": {
    "idNumber": "3",
    "gender": "Male",
    "age": 20,
    ....
  },
  "product": {
    "plan": "prod",
    "class": "1",
    "_type": "Product",
    ...
  }
}

And I want to receive new JSON with desired fields. I'm using next query:

SELECT 
    '{ "employeeInfo": {"age: ' + 
    JSON_VALUE(info, '$.employeeInfo.age') + ', "gender": ' + 
    JSON_VALUE(info, '$.employeeInfo.gender') + ' }' AS info
FROM 
    item.[Item] AS c
INNER JOIN 
    (SELECT "rootId", MAX("revisionNo") AS maxRevisionNo 
     FROM item."Item"
     WHERE "rootId" = 'E3B455EF-D48E-338C-B6D4-FFD8B41243F9' 
     GROUP BY "rootId") AS subquery ON c."rootId" = subquery."rootId";

And I get this response:

{ "employeeInfo": {"age: 38, "gender": Female }

But it seems JSON_VALUE doesn't return type, so Female is without quotes. I don't know what fields will be requested, so I can't add quotes myself. How can I execute a query to return values with their types. I.e. I expect next response:

{ "employeeInfo": {"age: 38, "gender": "Female" }

I'm using this SQL Server version:

Microsoft Azure SQL Edge Developer (RTM) - 15.0.2000.1552 (ARM64)

UPD: I'm already have solution for postgres:

SELECT jsonb_strip_nulls(json_build_object('employee_info', json_build_object('age', c."info"->'employeeInfo' -> 'age', 'gender', c."info"->'employeeInfo' -> 'gender'), 'product', c."info"->'product')::jsonb) as info ...

And also need to build a request for sql-server. I will build this request dynamically when receive field names.

UPD2: I've created one more question which contains additional requirements. I need "product.plan" field also and have possibility to add any other fields How to build JSON with selected fields from JSON columns in SQL server keeping fields type

Valeriy K.
  • 2,616
  • 1
  • 30
  • 53

2 Answers2

1

You may try to build the expected JSON with FOR JSON PATH and dot-separated column names for nested content, not with string concatenation. The documentation explains how FOR JSON converts SQL Server data types to JSON data types:

  • SQL Server character and string types (char, nchar, varchar, nvarchar) are converted to string JSON string data type.
  • SQL Server numeric types (int, bigint, float, decimal, numeric) are converted to JSON number data type.

Note, that with the additional INCLUDE_NULL_VALUES modifier, you may include the possible NULL values in the generated JSON.

SELECT 
   info = (
      SELECT 
         JSON_VALUE(info, '$.employeeInfo.age') AS "employeeInfo.age",
         JSON_VALUE(info, '$.employeeInfo.gender') AS "employeeInfo.gender"
      FOR JSON PATH, WITHOUT_ARRAY_WRAPPER 
   )
FROM ...

JSON_VALUE() always returns nvarchar(4000) and FOR JSON converts the values as JSON strings. If you need the actual data types, a solution is an explicit convertion with CONVERT() or an OPENJSON() call with explicit schema with actual columns data types.

SELECT 
   info = (
      SELECT age AS "employeeInfo.age", gender AS "employeeInfo.gender"
      FROM OPENJSON (info) WITH (
         age int '$.employeeInfo.age',
         gender varchar(10) '$.employeeInfo.gender'
      )
      FOR JSON PATH, WITHOUT_ARRAY_WRAPPER 
   )
FROM ...

SQL Server and Azure have limited JSON capabilities. If you do not know the actual data types of the referenced JSON properties, you need to call OPENJSON() with default schema and analyze the result (a table with columns key, value and type). A possible approach, parsing only the $.employeeInfo part of the stored JSON, is the following statement:

SELECT 
   info = (
      SELECT CONCAT('{"employeeInfo": {', STRING_AGG(t.Pair, ',') ,'}}') 
      FROM (
         SELECT 
            CONCAT(
               '"', [key], '":', 
               CASE 
                  WHEN [type] = 0 THEN 'null' 
                  WHEN [type] = 1 THEN CONCAT('"', STRING_ESCAPE([value], 'json'), '"') 
                  WHEN [type] = 2 THEN [value] 
                  WHEN [type] = 3 THEN [value] 
                  WHEN [type] = 4 THEN JSON_QUERY([value]) 
                  WHEN [type] = 5 THEN JSON_QUERY([value]) 
               END
            ) AS Pair 
         FROM OPENJSON (info, '$.employeeInfo')
         WHERE [key] IN ('age', 'gender') 
      ) t
   )
FROM ...
Zhorov
  • 28,486
  • 6
  • 27
  • 52
  • In this case, quotes are present in all values: '{"employeeInfo":{"age":"38","gender":"Female"}}' I don't need it for numbers – Valeriy K. Sep 01 '23 at 11:30
  • @ValeriyK., `JSON_VALUE()` always returns `nvarchar(4000)`. A solution is an explicit convertion with `CONVERT()` or an `OPENJSON()` call with explicit schema with actual column data types. – Zhorov Sep 01 '23 at 11:32
  • Or using normal column names, and `, ROOT('employeeInfo')`. Also use `'$.employeeInfo` in the `OPENJSON` call directly, rather than on each property. – Charlieface Sep 01 '23 at 11:43
  • @Charlieface, I agree. `OPENJSON(info, '$.employeeInfo')` is probably better in this specific situation. I included the full path syntax if the OP wants to access the values from different key (`product` for example). – Zhorov Sep 01 '23 at 11:53
  • It's not solve my issue. I receive fields in request and build requests to db on the fly - I don't know field types - only names – Valeriy K. Sep 01 '23 at 12:20
  • It seems he wants some dynamic solution that fetches fields from database and correctly types them? A bit like graphql – siggemannen Sep 01 '23 at 12:24
  • Yes, I want some dynamic solution. I already implemented it for postgres (see update) - I don't need to know field types for it – Valeriy K. Sep 01 '23 at 12:30
1

A slightly better syntax for @zhorov's excellent answer.

  • Use a ROOT parameter instead of adding it to the column name.
  • Put $.employeeInfo directly into OPENJSON
  • Use a window function instead of a self-join
SELECT 
   info = (
      SELECT
        j.age,
        j.gender
      FROM OPENJSON(i.info, '$.employeeInfo')
        WITH (
          age sql_variant,
          gender varchar(10)
        ) j
      FOR JSON PATH, WITHOUT_ARRAY_WRAPPER, ROOT('employeeInfo') 
   )
FROM (
    SELECT *,
      rn = ROW_NUMBER() OVER (PARTITION BY i.rootId ORDER BY i.revisionNo DESC)
    FROM item.Item AS i
    WHERE i.rootId = 'E3B455EF-D48E-338C-B6D4-FFD8B41243F9' 
) i
WHERE i.rn = 1;

If you want to do it completely dynamically then you need OPENJSON without a schema. Then rebuild it using a combination of STRING_AGG CONCAT and STRING_ESCAPE.

SELECT
  info = (
    SELECT
      CONCAT(
        '{"employeeInfo":{',
        STRING_AGG(
          CONCAT(
            '"',
            STRING_ESCAPE(j.[key], 'json'),
            '":',
            IIF(j.type = 1, CONCAT('"', STRING_ESCAPE(j.value, 'json'), '"'), j.value)
          ),
          ','
        ),
        '}}'
      )
    FROM OPENJSON(i.info, '$.employeeInfo') j
    WHERE j.[key] IN ('age', 'gender')
  )
FROM (
    SELECT *,
      rn = ROW_NUMBER() OVER (PARTITION BY i.rootId ORDER BY i.revisionNo DESC)
    FROM item.Item AS i
    WHERE i.rootId = 'E3B455EF-D48E-338C-B6D4-FFD8B41243F9' 
) i
WHERE i.rn = 1;
Charlieface
  • 52,284
  • 6
  • 19
  • 43
  • Thank you, but it hasn't resolved my issue. I receive fields in the request and dynamically generate database queries on the fly. I don't know the field types; I only have access to their names. – Valeriy K. Sep 01 '23 at 12:22
  • OK see new edit – Charlieface Sep 01 '23 at 12:33
  • Thanks, it works. But can I use it to get not only employeeInfo, but some additional fields, "product.plan" for example?. I don't see how I can add it to the FROM .. WHERE – Valeriy K. Sep 01 '23 at 14:05
  • 1
    It would help if you showed that in your question in the first place. I suggest you create a *new* question showing exactly what you are trying to do. – Charlieface Sep 01 '23 at 14:10
  • https://stackoverflow.com/questions/77024017/how-to-build-json-with-selected-fields-from-json-columns-in-sql-server-keeping-f – Valeriy K. Sep 01 '23 at 14:38
  • The solution is easy. To add another field it is required to add another SELECT CONCAT(... after 'info = (...)', like 'info = (SELECT CONCAT {"employeeInfo"....) + ', ' + (SELECT CONCAT {"product"....)' – Valeriy K. Sep 01 '23 at 15:08
  • Possibly, although it may be more efficient to use another method. For example, you could just do `FOR JSON PATH` on such a subquery. Without a full example it's hard to say – Charlieface Sep 01 '23 at 17:02