Monday, 10 April 2017

PL/SQL Object Types for JSON in Oracle Database 12c

  • Introduction

In Oracle Database 12c Release 1 the JSON functionality was focused on consuming JSON data and converting it into relation data. Generation of JSON relied on string handling or packages such as the APEX_JSON package. Oracle Database 12c Release 2  includes new JSON object types to support the in-memory parsing, generation and update of JSON data directly from PL/SQL.
  1. JSON_ELEMENT_T : The supertype some of the other object types extend. You will not often use this type directly. You can cast a JSON_ELEMENT_T to a subtype using TREAT AS. For example, "l_obj := TREAT (l_elem AS JSON_OBJECT_T);"
  2. JSON_OBJECT_T : An object representing a JSON object.
  3. JSON_ARRAY_T : An object representing a JSON array.
  4. JSON_SCALAR_T : A scalar value associated with a key such as a string, number, boolean or NULL.
  5. JSON_KEY_LIST : An array of key names, typically returned by the GET_KEYS method.

  • Constructors

You will typically create new instances of the object types in one of three ways.

SET SERVEROUTPUT ON
DECLARE
  l_obj JSON_OBJECT_T;
BEGIN
  -- New empty object using constructor.
  -- With or without the NEW keyword.
  l_obj := JSON_OBJECT_T();
  DBMS_OUTPUT.put_line('l_obj.stringify = ' || l_obj.stringify);
  l_obj := NEW JSON_OBJECT_T();
  DBMS_OUTPUT.put_line('l_obj.stringify = ' || l_obj.stringify);

  -- New object based on some JSON text using constructor.
  -- With or without the NEW keyword.
  l_obj := JSON_OBJECT_T('{ "employee_no":9999 }');
  DBMS_OUTPUT.put_line('l_obj.stringify = ' || l_obj.stringify);
  l_obj := NEW JSON_OBJECT_T('{ "employee_no":9999 }');
  DBMS_OUTPUT.put_line('l_obj.stringify = ' || l_obj.stringify);

  -- New object based on some JSON text using PARSE method.
  l_obj := JSON_OBJECT_T.parse('{ "employee_no":9999 }');
  DBMS_OUTPUT.put_line('l_obj.stringify = ' || l_obj.stringify);
END;
/
l_obj.stringify = {}
l_obj.stringify = {}
l_obj.stringify = {"employee_no":9999}
l_obj.stringify = {"employee_no":9999}
l_obj.stringify = {"employee_no":9999}

PL/SQL procedure successfully completed.

SQL>

  • Serialization Methods

From a JSON perspective, serialization means converting an object to a string, but in PL/SQL it means converting it to another data type. The contents of the JSON object types can be serialized using member functions or procedures, which support conversion to several data types. If the value being returned does not conform to the specific data type required, a conversion is attempted. The STRINGIFY and TO_STRING functions do the same thing, but the former will be more familiar to JavaScript developers.

 MEMBER FUNCTION STRINGIFY RETURNS VARCHAR2
 MEMBER FUNCTION TO_STRING RETURNS VARCHAR2
 MEMBER FUNCTION TO_BOOLEAN RETURNS BOOLEAN
 MEMBER FUNCTION TO_NUMBER RETURNS NUMBER
 MEMBER FUNCTION TO_DATE RETURNS DATE
 MEMBER FUNCTION TO_TIMESTAMP RETURNS TIMESTAMP

 MEMBER FUNCTION TO_CLOB RETURNS CLOB
 MEMBER PROCEDURE TO_CLOB

 Argument Name          Type                    In/Out         Default?
 ---------------------   -------------        ----------       --------
 C                                  CLOB                 IN/OUT     NOCOPY

 MEMBER FUNCTION TO_BLOB RETURNS BLOB
 MEMBER PROCEDURE TO_BLOB

 Argument Name          Type                    In/Out       Default?
 ---------------------  ----------------         ----------    --------
 B                                  BLOB                  IN/OUT      NOCOPY

  • Introspection Methods

The JSON object types include type-introspection member functions that can be used to identify their contents.

 MEMBER FUNCTION IS_OBJECT RETURNS BOOLEAN
 MEMBER FUNCTION IS_ARRAY RETURNS BOOLEAN
 MEMBER FUNCTION IS_SCALAR RETURNS BOOLEAN
 MEMBER FUNCTION IS_STRING RETURNS BOOLEAN
 MEMBER FUNCTION IS_NUMBER RETURNS BOOLEAN
 MEMBER FUNCTION IS_BOOLEAN RETURNS BOOLEAN
 MEMBER FUNCTION IS_TRUE RETURNS BOOLEAN
 MEMBER FUNCTION IS_FALSE RETURNS BOOLEAN
 MEMBER FUNCTION IS_NULL RETURNS BOOLEAN
 MEMBER FUNCTION IS_DATE RETURNS BOOLEAN
 MEMBER FUNCTION IS_TIMESTAMP RETURNS BOOLEAN
 MEMBER FUNCTION GET_SIZE RETURNS NUMBER
With the exception of the GET_SIZE method they should all be self explanatory. Depending on the object type, the GET_SIZE method returns the following.
  1. JSON_SCALAR_T : Returns 1
  2. JSON_OBJECT_T : Returns the number of top-level keys.
  3. JSON_ARRAY_T : Returns the number of array elements.

  • Error Handling

By default any problems when calling a member function result in a return value of NULL. This default behaviour can be altered by the ON_ERROR procedure which accepts one of the following values.

0 : Return NULL instead of raising an error (default).
1 : Raise all errors.
2 : Raise error if no value found.
3 : Raise error is return value doesn't match required data type.
4 : Raise error for invalid inputs.
The example below deliberately causes a data type conversion error, attempting to convert an employee name into a number. The first conversion error is ignored and a NULL is returned. After switching the error handling to "1", the error is raised.

SET SERVEROUTPUT ON
DECLARE
  l_obj JSON_OBJECT_T;
BEGIN
  l_obj := JSON_OBJECT_T('{ "employee_name":"CLARK" }');
  -- Default : l_obj.on_error(0); 
  DBMS_OUTPUT.put_line('Error 0 : l_obj.get_number = ' || l_obj.get_number('employee_name'));
  l_obj.on_error(1);
  DBMS_OUTPUT.put_line('Error 1 : l_obj.get_number = ' || l_obj.get_number('employee_name'));
END;
/
Error 0 : l_obj.get_number =
DECLARE
*
ERROR at line 1:
ORA-40566: JSON path expression selected a value of different data type.
ORA-06512: at "SYS.JDOM_T", line 418
ORA-06512: at "SYS.JSON_OBJECT_T", line 256
ORA-06512: at line 8

SQL>

Each object can have its error handling set separately.

  • Traversing and Amending JSON

The JSON object types include several member functions and procedures that allow JSON data to be traversed and amended. These member functions and procedures vary depending on the JSON object type.

The get style methods are summarised below.
  1. GET : Returns the JSON_ELEMENT_T associated with the specified key.
  2. GET_OBJECT : Returns the JSON_OBJECT_T associated with the specified key.
  3. GET_ARRAY : Returns the JSON_ARRAY_T associated with the specified key.
  4. GET_* : When applied to an object or array, it returns the data type specified in the method name associated with the specified key. Some JSON object types support calls to GET_STRING without a key. There is a member function and procedure for GET_GLOB and GET_BLOB.
  5. CLONE : Creates a full copy of the object specified by the key.
The following methods are used to amend the JSON data.
  1. PUT : For an object, amend the specified key with the specified value, or create it if it is missing. For an array, add a new element in the specified position, making room if the current index already exists. The examples below will demonstrate this difference.
  2. PUT_NULL : For an object, set the specified key value to NULL, or create it if it is missing. For an array, add a new NULL element in the specified position, making room if the current index already exists.
  3. REMOVE : Removed the specified key from an object, element in an array.
  4. RENAME_KEY : As the name suggests, it renames the specified key with the new value in an object.
  5. APPEND : Append a new element to the end of an array.
The use of these methods will become clearer when you examine the examples below.

  • Example 1: Amending a JSON Object

The following example creates a new JSON object, then amends it several times using the PUT, PUT_NULL, RENAME_KEY and REMOVE methods.

SET SERVEROUT ON FORMAT WRAPPED LINESIZE 1000
DECLARE
  l_obj JSON_OBJECT_T;
  
  PROCEDURE display (p_obj IN JSON_OBJECT_T) IS
  BEGIN
    DBMS_OUTPUT.put_line(p_obj.stringify);
  END;
BEGIN
  -- Create a new object.
  l_obj := JSON_OBJECT_T('{"employee_no":9999}');
  display(l_obj);
  
  -- Add a new element to the object.
  l_obj.put('a_string_value', 'A String');
  display(l_obj);

  -- Amend an existing element in the object.
  l_obj.put('a_string_value', 'A New String');
  display(l_obj);

  -- Rename the key of an existing element in the object.
  l_obj.rename_key('a_string_value', 'a_renamed_string_value');
  display(l_obj);

  -- Remove an element.
  l_obj.remove('a_renamed_string_value');
  display(l_obj);

  -- Add a new object element.
  l_obj.put('an_object_value', JSON_OBJECT_T('{"an_object":9999}'));
  display(l_obj);
  
  -- Remove an element.
  l_obj.remove('an_object_value');
  display(l_obj);

  -- Add a new array element.
  l_obj.put('an_array_value', JSON_ARRAY_T('["text","in","an","arry"]'));
  display(l_obj);

  -- Set an existing element to NULL.
  l_obj.put_null('an_array_value');
  display(l_obj);
END;
/
{"employee_no":9999}
{"employee_no":9999,"a_string_value":"A String"}
{"employee_no":9999,"a_string_value":"A New String"}
{"employee_no":9999,"a_renamed_string_value":"A New String"}
{"employee_no":9999}
{"employee_no":9999,"an_object_value":{"an_object":9999}}
{"employee_no":9999}
{"employee_no":9999,"an_array_value":["text","in","an","arry"]}
{"employee_no":9999,"an_array_value":null}

PL/SQL procedure successfully completed.

SQL>

  • Example 2: Amending a JSON Array

The following example creates a new JSON array, then amends it several times using the APPEND, APPEND_NULL, PUT, PUT_NULL and REMOVE methods.

SET SERVEROUT ON FORMAT WRAPPED LINESIZE 1000
DECLARE
  l_arr JSON_ARRAY_T;
  
  PROCEDURE display (p_arr IN JSON_ARRAY_T) IS
  BEGIN
    DBMS_OUTPUT.put_line(p_arr.stringify);
  END;
BEGIN
  -- Create a new array.
  l_arr := JSON_ARRAY_T('["apple","orange","banana"]');
  display(l_arr);
  
  -- Append a new element to the array.
  l_arr.append('pear');
  display(l_arr);
  
  -- Append a NULL element to the array.
  l_arr.append_null;
  display(l_arr);
  
  -- Add a new element to the array.
  -- 0-based, so 3 is position 4.
  l_arr.put(3, 'grape');
  display(l_arr);

  -- Add a NULL element to the array.
  -- 0-based, so 3 is position 4.
  l_arr.put_null(3);
  display(l_arr);

  -- Remove elements from the array.
  -- Notice reverse order, as positions are automatically shuffled.
  l_arr.remove(6);
  l_arr.remove(3);
  display(l_arr);

  -- Amend an element in the array.
  l_arr.remove(3);
  l_arr.put(3, 'pineapple');
  display(l_arr);

  -- Append a new object to the array.
  l_arr.append(JSON_OBJECT_T('{"grape":6}'));
  display(l_arr);

  -- Append a new array to the array.
  l_arr.append(JSON_ARRAY_T('[1,2,3,4,5]'));
  display(l_arr);
  
END;
/
["apple","orange","banana"]
["apple","orange","banana","pear"]
["apple","orange","banana","pear",null]
["apple","orange","banana","grape","pear",null]
["apple","orange","banana",null,"grape","pear",null]
["apple","orange","banana","grape","pear"]
["apple","orange","banana","pineapple","pear"]
["apple","orange","banana","pineapple","pear",{"grape":6}]
["apple","orange","banana","pineapple","pear",{"grape":6},[1,2,3,4,5]]

PL/SQL procedure successfully completed.

SQL>

  • Example 3 : Parse JSON Data 1

The examples in the remainder of this article require the following table.

CREATE TABLE json_documents (
  id    NUMBER,
  data  CLOB,
  CONSTRAINT json_documents_pk PRIMARY KEY (id),
  CONSTRAINT json_documents_json_chk CHECK (data IS JSON)
);

INSERT INTO json_documents (id, data)
VALUES (1, '{
  "departments":[
    {
      "department":{
        "department_name":"DEV",
        "department_no":70,
        "employees":[
          {
            "employee_number":9000,
            "employee_name":"JONES",
            "salary":1000
          },
          {
            "employee_number":9001,
            "employee_name":"SMITH",
            "salary":2000
          }
        ]
      }
    },
    {
      "department":{
        "department_name":"DBA",
        "department_no":80,
        "employees":[
          {
            "employee_number":9002,
            "employee_name":"HALL",
            "salary":3000
          }
        ]
      }
    }
  ]
}');

COMMIT;

In the following example we know the basic structure of the JSON data, so we loop through the departments array and nested employees array, explicitly requesting the keys of interest to display the data.

SET SERVEROUT ON FORMAT WRAPPED LINESIZE 1000
DECLARE
  l_clob            CLOB;
  l_top_obj       JSON_OBJECT_T;
  l_dept_arr      JSON_ARRAY_T;
  l_dept_obj     JSON_OBJECT_T;
  l_emp_arr      JSON_ARRAY_T;
  l_emp_obj      JSON_OBJECT_T;
BEGIN
  
  SELECT data
  INTO   l_clob
  FROM   json_documents
  WHERE  id = 1;
  
  l_top_obj := JSON_OBJECT_T(l_clob);

  l_dept_arr := l_top_obj.get_array('departments');
  
  FOR i IN 0 .. l_dept_arr.get_size - 1 LOOP
    DBMS_OUTPUT.put_line('-------------------------------------------');
    l_dept_obj := TREAT(l_dept_arr.get(i) AS JSON_OBJECT_T).get_object('department');
    DBMS_OUTPUT.put_line('index           : ' || i);
    DBMS_OUTPUT.put_line('department_name : ' || l_dept_obj.get_string('department_name'));
    DBMS_OUTPUT.put_line('department_no   : ' || l_dept_obj.get_number('department_no'));

    l_emp_arr := l_dept_obj.get_array('employees');
    FOR j IN 0 .. l_emp_arr.get_size - 1 LOOP
      l_emp_obj := TREAT(l_emp_arr.get(j) AS JSON_OBJECT_T);
      DBMS_OUTPUT.put_line('  --');
      DBMS_OUTPUT.put_line('  index           : ' || j);
      DBMS_OUTPUT.put_line('  employee_number : ' || l_emp_obj.get_number('employee_number'));
      DBMS_OUTPUT.put_line('  employee_name   : ' || l_emp_obj.get_string('employee_name'));
      DBMS_OUTPUT.put_line('  salary          : ' || l_emp_obj.get_number('salary'));
    END LOOP;
  END LOOP;
END;
/
-------------------------------------------
index           : 0
department_name : DEV
department_no   : 70
  --
  index           : 0
  employee_number : 9000
  employee_name   : JONES
  salary          : 1000
  --
  index           : 1
  employee_number : 9001
  employee_name   : SMITH
  salary          : 2000
-------------------------------------------
index           : 1
department_name : DBA
department_no   : 80
  --
  index           : 0
  employee_number : 9002
  employee_name   : HALL
  salary          : 3000

PL/SQL procedure successfully completed.

SQL>

  • Example 4 : Parse JSON Data 2

In the following example, we understand the basic structure of the JSON data, an array of departments, with each department containing an array of employees, but we are not sure what keys are present in each department and employee, so we can't request them by name. Instead, we loop through the key list and use introspection to decide how to process the values associated with the keys.

SET SERVEROUT ON FORMAT WRAPPED LINESIZE 1000
DECLARE 
  l_clob                      CLOB;
  l_top_obj                JSON_OBJECT_T;
  l_dept_arr               JSON_ARRAY_T;
  l_dept_obj              JSON_OBJECT_T;
  l_dept_key_list      JSON_KEY_LIST;
  l_emp_arr              JSON_ARRAY_T;
  l_emp_obj              JSON_OBJECT_T;
  l_emp_key_list      JSON_KEY_LIST;
BEGIN
  
  SELECT data
  INTO   l_clob
  FROM   json_documents
  WHERE  id = 1;
  
  l_top_obj := JSON_OBJECT_T(l_clob);
  l_dept_arr := l_top_obj.get_array('departments');
  
  << departments_loop >>
  FOR i IN 0 .. l_dept_arr.get_size - 1 LOOP
    DBMS_OUTPUT.put_line('-------------------------------------------');
    DBMS_OUTPUT.put_line('index : ' || i);
    l_dept_obj := TREAT(l_dept_arr.get(i) AS JSON_OBJECT_T).get_object('department');
    l_dept_key_list := l_dept_obj.get_keys;
    
    << department_keys_loop >>
    FOR j IN 1 .. l_dept_key_list.count LOOP
      IF l_dept_obj.get(l_dept_key_list(j)).is_array THEN
        -- This element contains an array, so we must process the array.
        l_emp_arr := TREAT(l_dept_obj.get(l_dept_key_list(j)) AS JSON_ARRAY_T);
        
        << employees_loop >>
        FOR j IN 0 .. l_emp_arr.get_size - 1 LOOP
          DBMS_OUTPUT.put_line('  --');
          DBMS_OUTPUT.put_line('  index : ' || j);
          IF l_emp_arr.get(j).is_object THEN
            l_emp_obj := TREAT(l_emp_arr.get(j) AS JSON_OBJECT_T);
            l_emp_key_list := l_emp_obj.get_keys;
            
            << employee_keys_loop >>
            FOR k IN 1 .. l_emp_key_list.count LOOP
              DBMS_OUTPUT.put_line('  ' || l_emp_key_list(k) || ' : ' || l_emp_obj.get_string(l_emp_key_list(k)));
            END LOOP;
            NULL;
          ELSE
            DBMS_OUTPUT.put_line('We were expecting an employee to be an object!');
          END IF;
        END LOOP;

      ELSIF l_dept_obj.get(l_dept_key_list(j)).is_number THEN
        -- It is a number.
        DBMS_OUTPUT.put_line(l_dept_key_list(j) || ' : ' || l_dept_obj.get_number(l_dept_key_list(j)));
      ELSIF l_dept_obj.get(l_dept_key_list(j)).is_string THEN
        -- It is a number.
        DBMS_OUTPUT.put_line(l_dept_key_list(j) || ' : ' || l_dept_obj.get_string(l_dept_key_list(j)));
      ELSE
        -- Catch-all. Stringify.
        DBMS_OUTPUT.put_line(l_dept_key_list(j) || ' : ' || l_dept_obj.get(l_dept_key_list(j)).stringify);
      END IF;
    END LOOP;
  END LOOP;
END;
/
-------------------------------------------
index : 0
department_name : DEV
department_no : 70
  --
  index : 0
  employee_number : 9000
  employee_name : JONES
  salary : 1000
  --
  index : 1
  employee_number : 9001
  employee_name : SMITH
  salary : 2000
-------------------------------------------
index : 1
department_name : DBA
department_no : 80
  --
  index : 0
  employee_number : 9002
  employee_name : HALL
  salary : 3000

PL/SQL procedure successfully completed.

SQL>