A recent forum post prompted me to look into JSON syntax rules and their implications more closely. As per the JSON Developer’s Guide, there are two aspects to the topic of lax and strict JSON:

  • When storing JSON as a database column
  • Performing type conversions as part of JSON functions like JSON_QUERY, etc.

Let’s look at the second aspect: type conversion. This post concerns Oracle Database 23ai and JSON SQL functions.

Before diving into the third part let’s recap the gist about type conversions in JSON processing. If you read part 1 and 2 (link to part 1 | link to part 2), feel free to skip straight to SQL JSON functions.

Type conversions

JSON data is schemaless; in other words, it is very tolerant towards how you use it. There is a limited number of data types available but they aren’t always enforced. You can easily store a number as a string, and that’s perfectly fine. Many systems perform an implicit type conversion: they will try to cast the string to a number, and only if that fails an error is thrown.

Sometimes, however, you want finer control over how data is treated. If something is stored as a string ({ "a": "123" }), you may want to ensure that it is treated as such and not as a number.

Here is an example. Consider a bunch of JSON documents:

with sample_data(jcol) as (
        values
            (JSON('{ "a": { "b": { "c": "1" } } }')),
            (JSON('{ "a": { "b": { "c":  2 } } }')),
            (JSON('{ a:   { b  : {  c:  3 } } }'))
    )
    select
        rownum rn,
        jcol
    from
        sample_data;
    

Keen eyes have spotted that the 3rd JSON document follows the “lax” syntax discussed in part 1.

If you run the query above, you get the following result:

   RN J                        
    _____ ________________________ 
        1 {"a":{"b":{"c":"1"}}}    
        2 {"a":{"b":{"c":2}}}      
        3 {"a":{"b":{"c":3}}}
    

Even though { a: { b : { c: 3 } } } was passed verbatim like this – in other words, lax JSON – to the JSON constructor, it returned “strict” JSON. Also note how the first row’s “c” value is a string rather than a number. This will become important later on.

SQL JSON functions

The SQL standard has been enhanced in 2016 and 2023 to allow for better integration with JSON. Oracle Database has been one of the earliest adopters of the new standard functions, let’s look at some of them in more details. To keep this post reasonably short, it focuses only on those that allow the specification of a TYPE clause, that is, disable implicit type conversion.

The following functions feature a TYPE clause:

  • JSON_VALUE
  • JSON_QUERY
  • JSON_TABLE
  • JSON_EXISTS
  • JSON_TRANSFORM

Let’s look at what that means using json_value as an example.

JSON_VALUE

Most often you use JSON_VALUE if you want to extract a scalar from a JSON document. You can alternatively select arrays or objects, but that requires the use of a collection (return) type and is out of scope of this article.

The following query provides sample data for the purpose of this discussion:

with sample_data(jcol) as (
        values
            (JSON('{ "a": { "b": { "c": "1" } } }')),
            (JSON('{ "a": { "b": { "c":  2 } } }')),
            (JSON('{ "a": { "b": { "c":  3 } } }'))
    )
    select
        rownum rn,
        jcol
    from
        sample_data;
    

Let’s assume you want to extract the numeric value of "c" – “1”, 2, and 3 in the example. Using json_value you can do that as follows:

with sample_data(jcol) as (
        values
            (JSON('{ "a": { "b": { "c": "1" } } }')),
            (JSON('{ "a": { "b": { "c":  2 } } }')),
            (JSON('{ "a": { "b": { "c":  3 } } }'))
    )
    select
        rownum rn,
        json_value(
            jcol,
            '$.a.b.c'
            returning number
        ) as c,
        dump(
            json_value(
                jcol,
                '$.a.b.c'
                returning number
            )
        ) as type_c
    from
        sample_data;
    

Here is the query result. Typ=2 indicates a number, so we’re fine. json_value returns a VARCHAR2 by default, hence the need to tell it to return a number, as per the requirement.

   RN    C TYPE_C                
    _____ ____ _____________________ 
        1    1 Typ=2 Len=2: 193,2    
        2    2 Typ=2 Len=2: 193,3    
        3    3 Typ=2 Len=2: 193,4 
    

As you can see, Oracle implicitly converted the string "1" to a number. If you don’t want this conversion to happen, you tell json_value not to convert strings to numbers using the type (strict) clause. Parentheses are not optional by the way! The other option, type (lax) is the function’s default. Let’s see what that does:

with sample_data(jcol) as (
        values
            (JSON('{ "a": { "b": { "c": "1" } } }')),
            (JSON('{ "a": { "b": { "c":  2 } } }')),
            (JSON('{ "a": { "b": { "c":  3 } } }'))
    )
    select
        rownum rn,
        json_value(
            jcol,
            '$.a.b.c'
            returning number
            type (strict)
        ) as c,
        dump(
            json_value(
                jcol,
                '$.a.b.c'
                returning number
                type (strict)
            )
        ) as type_c
    from
        sample_data;
    

The database returns the following:


       RN    C TYPE_C                
    _____ ____ _____________________ 
        1      NULL                  
        2    2 Typ=2 Len=2: 193,3    
        3    3 Typ=2 Len=2: 193,4
    

Now you can see that the string in row 1 wasn’t converted to a number, and you get a NULL in return; this is the default. If you want to be absolutely sure not to accidentally miss incorrect input data (because you’re rather safe than sorry), you can tell the database to throw an error using the error on error clause:

Error starting at line : 1 in command -
    with sample_data(jcol) as (
        values
            (JSON('{ "a": { "b": { "c": "1" } } }')),
            (JSON('{ "a": { "b": { "c":  2 } } }')),
            (JSON('{ "a": { "b": { "c":  3 } } }'))
    )
    select
        rownum rn,
        json_value(
            jcol,
            '$.a.b.c'
            returning number
            error on error
            type (strict)
        ) as c,
        dump(
            json_value(
                jcol,
                '$.a.b.c'
                returning number
                error on error
                type (strict)
            )
        ) as type_c
    from
        sample_data
    Error at Command Line : 5 Column : 15
    Error report -
    SQL Error: ORA-03302: (ORA-01722 details) invalid string value: 1
    ORA-61724: unable to convert value selected by JSON path '$.a.b.c'
    ORA-01722: unable to convert string value containing '1' to a number: JCOL
    

The ability to change the behaviour on error is one of the major differences between SQL JSON functions and the simple dot access notation shown in a previous article. In scenarios where it is absolutely essential to ensure that data types match expectations type (strict), potentially combined with error on error comes in very handy.

By the way, you could have added a type conversion item method to the JSON path expression ($.a.b.c.numberOnly()) as well, but that’s not in scope of this article 🧐

Other SQL JSON functions

As you read earlier, there are additional SQL JSON function that accept the TYPE clause. Let’s look at a few examples, please refer to the JSON Developer’s Guide Part IV and the SQL Language Reference for Oracle Database 23ai as well.

Here is an example for JSON_QUERY and the TYPE clause.

with sample_data(jcol) as (
        values
            (JSON('{ "a": { "b": { "c": "1", "d": [ 1, 2, "3" ] } } }')),
            (JSON('{ "a": { "b": { "c":  2 , "d": [ "1", 2, 3 ] } } }')),
            (JSON('{ "a": { "b": { "c":  3 , "d": [ 1, 2, 3 ] } } }'))
    )
    select
        rownum rn,
        json_query(
            jcol,
            '$.a.b.d?(@ == 1)' type (lax)
        ) json_query,
        json_query(
            jcol,
            '$.a.b.d?(@ == 1)' type (strict)
        ) json_query_strict
    from
        sample_data;
    

The database returns the following:

   RN JSON_QUERY    JSON_QUERY_STRICT    
    _____ _____________ ____________________ 
        1 [1,2,"3"]     [1,2,"3"]            
        2 ["1",2,3]                          
        3 [1,2,3]       [1,2,3] 
    

As you can see the second row’s value of d ([ "1", 2, 3 ]) is discarded by json_query because it doesn’t contain the number 1 requested by the JSON Patch expression. it contains a string "1" that is implicitly cast by default or when specifying type (lax). Agagin, the type (strict) clause makes sense here because you ensure the input is in the exact form you want it.

JSON_EXISTS is a bit of an odd one, because you typically use it in where clauses and not in the select-list like the other functions you read about.

with sample_data(jcol) as (
        values
            (JSON('{ "a": { "b": { "c": "1" } } }')),
            (JSON('{ "a": { "b": { "c":  2  } } }')),
            (JSON('{ "a": { "b": { "c":  3 } } }'))
    )
    select
        jcol
    from
        sample_data
    where
        json_exists (
            jcol,
            '$.a.b?(@.c in (1,2,3))' type (strict)
        );
    

The strict type clause prevents the type conversion for the first JSON document { "c": "1" } does not match the JSON Path expression

JCOL                   
    ______________________ 
    {"a":{"b":{"c":2}}}    
    {"a":{"b":{"c":3}}}
    

Another example, this time for JSON_TABLE. With this rather complex SQL function you transform JSON into a relational format. The below example is rather thorough, as it demonstrates how to break nested JSON down into relational table format. The TYPE clause can be applied to each column definition as you can see.

with sample_data(jcol) as (
        values
            (JSON('{ "a": { "b": { "c": "1", "d": [ { "id": 1, "val": 1 },   { "id": 2, "val": 2   } ] } } }')),
            (JSON('{ "a": { "b": { "c":  2 , "d": [ { "id": 1, "val": 2 },   { "id": 2, "val": "3" } ] } } }')),
            (JSON('{ "a": { "b": { "c":  3 , "d": [ { "id": 1, "val": "3" }, { "id": 2, "val": 4   } ] } } }'))
    )
    select
        jt.*
    from
        sample_data s,
        json_table(
            s.jcol,
            '$.a.b'
            columns (
                c_lax    number,
                c_strict number path '$.c' type (strict),
                nested d[*] columns (
                    id         number path '$.id',
                    val_lax    number path '$.val',
                    val_strict number path '$.val' type (strict) 
                )
            )
        ) jt
    /
    

The database returns

   C_LAX    C_STRICT    ID    VAL_LAX    VAL_STRICT 
    ________ ___________ _____ __________ _____________ 
                             1          1             1 
                             2          2             2 
                       2     1          2             2 
                       2     2          3               
                       3     1          3               
                       3     2          4             4
    

Again, those values provided as strings aren’t cast to numbers. As with all other SQL JSON functions the default is to return NULL in that case.

Summary

This post is veeeery long, I guess it’s most useful as a cheat sheet rather than something you read from the front to back. There is a lot to be said about JSON and the relational world, I hope this is a good start. If you have further examples you’d like to see, please let me know!