March/April 2018
Oracle Database 12c Release 1 ushered in a new age of JSON inside Oracle Database through wide-ranging and smart support of JSON documents in SQL. You can use SQL to join JSON data with relational data. You can “project” JSON data relationally, extracting content from within the JSON document, thereby making it available for relational processes and tools. You can even query, right from within the database, JSON data that is stored outside Oracle Database in an external table.
Oracle’s Live SQL provides developers a free and easy online way to test and share SQL and PL/SQL application development concepts. |
Oracle Database 12c Release 2 added several predefined object types to PL/SQL to allow fine-grained programmatic construction and manipulation of in-memory JSON data. You can introspect it, modify it, and serialize it back to textual JSON data.
Why Use PL/SQL with JSON Data?
If you can query and manipulate JSON data in Oracle Database tables with SQL, why do you also need PL/SQL object types? You can use the object types to programmatically manipulate JSON data in memory, to do things such as the following:
Overview
The primary PL/SQL JSON object types you will use are JSON_ELEMENT_T, JSON_OBJECT_T, JSON_ARRAY_T, and JSON_SCALAR_T.
Instances of these types are not persistent. Instead, you first read data from a table into the appropriate instances of the object types or construct the instances through parsing and other operations directly in your PL/SQL code. Next, you manipulate the instances as needed, modifying, removing, or adding values. Finally, you convert or serialize them to VARCHAR2 or large object (LOB) data. Then you store that serialized data back into the database table—or pass it along to a consumer of this data, such as a JavaScript-based web application.
As with so many other data structures manipulated in PL/SQL, you don’t have to worry about freeing up the memory associated with these instances. The database will automatically perform garbage collection tasks. Thanks, Oracle Database!
Let’s explore the new PL/SQL JSON functionality in Oracle Database 12.2 through a series of simple examples.
Extract JSON Values
I first create and populate my species table with very simple JSON data:
CREATE TABLE json_species
(
id NUMBER NOT NULL PRIMARY KEY,
info CLOB CONSTRAINT is_json CHECK (info IS JSON ) )
/
BEGIN
INSERT INTO json_species
VALUES (1, '{"name":"Spider"}');
INSERT INTO json_species
VALUES (2, '{"name":"Elephant", "trunk_length":"10"}');
INSERT INTO json_species
VALUES (3, '{"name":"Shark", "fin_count":"4"}');
COMMIT;
END;
/
I can use SQL to query the values of specific items within the JSON documents, as follows:
SELECT sp.info.name
FROM json_species sp
But I can also use the PL/SQL API:
DECLARE
CURSOR species_cur
IS
SELECT id, sp.info
FROM json_species sp;
l_info json_element_t;
l_document_object json_object_t;
l_name json_element_t;
BEGIN
FOR rec IN species_cur
LOOP
l_info := json_element_t.parse (rec.info);
l_document_object := TREAT (l_info AS json_object_t);
l_name := l_document_object.get ('name');
dbms_output.put_line (l_name.to_string);
end loop;
END;
/
The JSON_ELEMENT_T.parse method converts the JSON text into a hierarchical structure that can be traversed using methods in the object types.
I then use TREAT to cast the JSON element instance to a JSON object. Next, I use the get method of JSON_OBJECT_T to get the value for the name attribute. Then I use the to_string method to convert that value into a string.
For this very simple requirement of displaying the names, in which I am not going to iterate through the parsed structure, I could also do this:
BEGIN
FOR rec IN (SELECT sp.info FROM json_species sp
ORDER BY sp.info.name)
LOOP
dbms_output.put_line (
json_object_t (rec.info).get ('name').to_string);
END LOOP;
END;
In this block, I take advantage of the object-oriented ability to string together multiple invocations of methods. First, I call the constructor function for JSON_OBJECT_T, passing it the JSON character large object (CLOB) data. That returns an instance of JSON_OBJECT_T. Then I call its get method, passing it the name of the attribute (name), and then I invoke the to_string method.
Now what if I want to change the names?
Update JSON Values
The first and most important thing to remember about changing the contents of a JSON document stored in a table is that you must replace the entire document. You cannot, through a SQL UPDATE statement, change the values of individual attributes in the document.
Instead, you retrieve the document from the table, make changes to the document’s contents using the PL/SQL API in Oracle Database 12.2 or 12.1 string operations on the JSON text, and then use a SQL UPDATE to replace the entire document.
Let’s go through the steps needed to implement the following requirement for my species table: all names need to be in uppercase. The code for the requirement is in Listing 1.
Listing 1: Use PL/SQL object types for JSON to change all names to uppercase.
DECLARE
CURSOR species_cur
IS
SELECT sp.id, sp.info
FROM json_species sp
FOR UPDATE;
l_species json_object_t;
l_species_for_update json_species.info%TYPE;
l_current_name VARCHAR2 (1000);
l_new_name VARCHAR2 (1000);
BEGIN
FOR rec IN species_cur
LOOP
l_species := json_object_t (rec.info);
l_current_name := l_species.get ('name').to_string;
l_new_name := TRIM (BOTH '"' FROM UPPER (l_current_name));
l_species.put ('name', l_new_name);
l_species_for_update := l_species.stringify;
UPDATE json_species
SET info = l_species_for_update
WHERE CURRENT OF species_cur;
END LOOP;
END;
And here is an explanation of that code, line by line:
Line(s) | Explanation |
2–6 | Declare a cursor to iterate through all the species in the table. |
8 | Declare a JSON object instance to hold the value returned from the query. |
9 | Declare a CLOB variable that will be used to update the table after the name has been converted to uppercase. |
10 | Declare a local variable to hold the current, lowercase name. |
11 | Declare a local variable to hold the new, uppercase name. |
15 | Get the CLOB containing the JSON text from the table and convert it to a JSON object. |
17 | Call the get method to obtain the current value of the name. |
19 | Use the TRIM function to remove both the leading and trailing double quote mark around the string. Without this, the put method on line 21 will convert each quote mark to \" in order to “escape” it. You don’t want that. |
21 | Call the put method to change the value of the name in the JSON object. |
23 | Convert the JSON object into a CLOB. |
25–27 | Update the entire JSON column value with the uppercase name. |
As you get more comfortable with these PL/SQL object types for JSON and their methods, you will look at that code and say “We don’t need to do all that, Steven!” And you will be right. You can replace lines 17 though 21 with a single statement:
l_species.put (
'name',
TRIM (BOTH '"' FROM
UPPER (l_species.get ('name').to_string)));
But when you are first working with these types, you will likely want to trace each step of the transformation to make sure you get it right. Local variables help with that.
As you might expect, there’s more than one method for modifying the contents of a JSON document. PL/SQL object types for JSON also offer the following methods.
l_species.put_null ('name');
l_species.rename_key ('name', 'species_name');
l_species.remove ('trunk_length');
You can also use PUT, PUT_NULL, and REMOVE to modify arrays, which I cover in the next section.
Working with JSON Arrays
Most JSON documents aren’t nearly so simple as the values inserted earlier into the JSON_SPECIES.INFO column. For example, they often contain arrays, indicated by square brackets, as in the following:
BEGIN
INSERT INTO json_species
VALUES (10, '{" name":"Ostrich",
"favorite_eats":
["Stirfry", "Yogurt", "Mosquitos"]}');
END;
The very first thing you should accept—and remember—about JSON arrays is that the first index value is 0, not 1 (as is the case for PL/SQL nested tables and varrays).
With that out of the way, let’s look at how to use JSON_ARRAY_T to work with JSON arrays.
Here are some of the methods you will find helpful for array manipulation:
Here are a number of examples using these methods. In each example, assume that the block is executed from within an outer block that declared and populated a JSON object instance as follows:
DECLARE
l_ostrich json_object_t;
l_eats json_array_t;
BEGIN
l_ostrich :=
json_object_t (
'{"name":"Ostrich",
"favorite_eats":
["Stirfry", "Yogurt", "Mosquitos"]}');
...example block here...
END;
DECLARE
l_eats json_array_t;
BEGIN
l_eats := TREAT (l_ostrich.get ('favorite_eats') AS json_array_t);
DBMS_OUTPUT.put_line ('# of eats = ' || l_eats.get_size);
END;
DECLARE
l_eats json_array_t;
BEGIN
IF l_ostrich.get ('favorite_eats').is_array ()
THEN
DBMS_OUTPUT.put_line ('favorite eats is an array');
END IF;
IF NOT l_ostrich.get ('name').is_array ()
THEN
DBMS_OUTPUT.put_line ('name is NOT an array');
END IF;
END;
favorite eats is an array
name is NOT an array
BEGIN
l_eats.APPEND ('Truffles');
DBMS_OUTPUT.put_line (l_eats.stringify());
END;
/
["Stirfry","Yogurt","Mosquitos","Truffles"]
BEGIN
l_eats.APPEND_NULL;
DBMS_OUTPUT.put_line (l_eats.stringify());
END;
/
["Stirfry","Yogurt","Mosquitos",null]
BEGIN
/* Add Ice Cream before Yogurt.
Remember: arrays start at 0 */
l_eats.put (1, 'Ice Cream');
DBMS_OUTPUT.put_line (l_eats.stringify());
/* Add a null value before Ice Cream */
l_eats.put_null (1);
DBMS_OUTPUT.put_line (l_eats.stringify());
/* Replace that null with Broccoli */
l_eats.REMOVE (1);
l_eats.put (1, 'Broccoli');
DBMS_OUTPUT.put_line (l_eats.stringify());
END;
/
["Stirfry","Ice Cream","Yogurt","Mosquitos"]
["Stirfry",null,"Ice Cream","Yogurt","Mosquitos"]
["Stirfry","Broccoli","Ice Cream","Yogurt","Mosquitos"]
Notice that to change a value in an array, I had to first remove it, and then I put a new value in its place.
Error Handling with JSON Types
When it comes to handling errors, the behavior of the JSON types is a bit different from the default behavior in PL/SQL. Generally, if an operation in SQL or PL/SQL results in an error, an exception is raised, halting execution of the block.
You have more flexibility when it comes to the JSON types. First of all, the default behavior is that if an error occurs when an operation calls a member function (a function called for the instance of a type versus a static function, which is invoked on the type itself), that function returns a NULL value.
You can change this behavior by calling the on_error method and passing it one of the following values:
Value | Action Performed |
0 | Reset to the default behavior, which is to return NULL instead of raising an error. |
1 | Raise all errors. |
2 | Raise an error if no value is detected. |
3 | Raise an error if the data types do not match. |
4 | Raise an error if the input is invalid. Example: an array reference is out of bounds. |
In the following block, I rely on the default behavior. As a result, Number = is displayed, and no error is raised.
DECLARE
l_fav json_object_t;
l_num NUMBER;
BEGIN
l_fav := json_object_t ('{"favorite_flavor":"chocolate"}');
/* The default */
l_fav.on_error (0);
l_num := l_fav.get_number ('favorite_flavor');
DBMS_OUTPUT.put_line ('Number = ' || l_num);
END;
Now I set the error handling value to 1 in the on_error method to raise all errors. When I execute this block:
DECLARE
l_fav json_object_t;
l_num NUMBER;
BEGIN
l_fav := json_object_t ('{"favorite_flavor":"chocolate"}');
/* Raise all errors */
l_fav.on_error (1);
l_num := l_fav.get_number ('favorite_flavor');
END;
I see the following error:
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 10
Now, what if I try to access an array position that is out of bounds?
DECLARE
l_eats json_array_t;
BEGIN
l_eats := JSON_ARRAY_T ('["Stirfry", "Yogurt", "Mosquitos"]');
/* Raise all errors */
l_eats.on_error (4);
l_eats.put (-1, 'Ice Cream');
DBMS_OUTPUT.PUT_LINE (l_eats.get_size());
END;
I see the following error:
ORA-40578: invalid or non-existent array position
ORA-06512: at "SYS.JDOM_T", line 255
ORA-06512: at "SYS.JSON_ARRAY_T", line 192
ORA-06512: at line 9
Chasing JSON to a Conclusion
Almost every new application being built today, even those constructed in Oracle Application Express, rely heavily on JavaScript and JSON. It’s easy for front-end developers to think that Oracle Database is good only for “relational data” and that if they want data in a JSON format, they need to look elsewhere, perhaps in a document or NoSQL database.
This is definitely and demonstrably not the case, especially with Oracle Database 12.2.
Database developers should learn about JSON in Oracle Database and get really good at building APIs with PL/SQL packages and Oracle REST Data Services. That way, they can provide all the JSON their front-end developers need while ensuring a high level of performance and security for application data.
This article gives you a starting point for manipulating JSON in PL/SQL. Future articles will explore the topic in greater detail.
Next Steps
READ more about Oracle Database support for JSON (documentation).
READ more about Steven Feuerstein’s blog post on getting started with JSON in Oracle Database.
LEARN more about Oracle Database support for JSON (tutorial).
EXPLORE code used in this article at Oracle’s Live SQL.
Photography by © Hero/Corbis
Steven Feuerstein was Oracle Corporation's Developer Advocate for PL/SQL between 2014 and 2021. He is an expert on the Oracle PL/SQL language, having written ten books on PL/SQL, including Oracle PL/SQL Programming and Oracle PL/SQL Best Practices (all published by O'Reilly Media), and currently serving as Senior Advisor for insum Solutions. Steven has been developing software since 1980, spent five years with Oracle back in the "old days" (1987-1992), and was PL/SQL Evangelist for Quest Software (and then Dell) from January 2001 to February 2014 - at which point he returned joyfully to Oracle Corporation. He was one of the original Oracle ACE Directors and writes regularly for Oracle Magazine, which named him the PL/SQL Developer of the Year in both 2002 and 2006. He is also the first recipient of ODTUG's Lifetime Achievement Award (2009).