14 Exposing APIs for Integration#

Expose REST APIs for external integrations while reusing business logic and securing access.

A PL/SQL package provides an application programming interface (API) for any solution in your workspace. However, when external systems need to integrate with your app, you need to provide them a REST API instead. It's a web service that another program can call over the network, and uses JavaScript Object Notation (JSON) as a simple way to send and receive data.

Some integrations only need to read data, while others may require creating, updating, or deleting it. If you let an outside system change your application data, you must apply the same validation and defaulting business logic used in your web user interface to keep bad data out of your system of record. In either case, you need to ensure only intended clients can access your services.

This section's example code helps you witness how two key Oracle 26ai features can cut the effort to expose APIs in half.

14.1 Reviewing Action Items API Requirements#

Review why an external system needs REST APIs to integrate with your APEX app.

An external support ticketing system needs to integrate with your existing application managing action items and the team of staff members assigned to work on each one. Review the app's functionality and how the external tool needs to integrate with it.

14.2 Reviewing REST API Basics#

Review the HTTP, JSON, SQL, and PL/SQL basics behind REST APIs.

Before learning how to expose a REST API for your application, it's good to review the basic "ingredients" involved in the recipe:
  • Key HTTP methods that REST APIs use,
  • The basic syntax of JSON documents,
  • SQL features to construct JSON from relational data, and
  • PL/SQL's native JSON types, and
  • Simplifications JSON Relational Duality Views enable in Oracle 26ai.

14.3 Planning Your ORDS Service Module#

Plan your ORDS service module schema alias, URL templates, and handlers.

Oracle REST Data Services (ORDS) lets you define web APIs using SQL and PL/SQL. Using Oracle 26ai's multilingual engine (MLE), you can also define them with server-side JavaScript.

14.4 Using SQL Developer Web REST Designer#

Use SQL Developer Web REST Designer to create and navigate ORDS service definitions.

Access SQL Developer Web directly from App Builder to define your ORDS modules, templates, and handlers.

14.5 Creating and Testing Read-Only APIs#

Create and test read-only REST APIs for collections and single rows.

When your use case calls for reading data, you can build REST APIs using SQL queries. A Collection Query handler lets clients filter, sort, and page through the results, while a Collection Item handler returns a single row. You can test your REST APIs using curl on the command line or tools with a graphical interface like Postman.

14.6 Enforcing Business Logic in REST APIs#

Apply the same business rules to REST API changes that your APEX pages use.

If you let an outside system change your application data, you must apply the same validation and defaulting business logic used in your web user interface to keep bad data out of your system of record.

14.7 Controlling REST API Behavior Completely#

For complete control over your ORDS REST APIs, create a full set of PL/SQL handlers.

Tip:

Your module templates can mix Collection Query and Collection Item GET handlers with PL/SQL handlers for POST, PUT, and DELETE operations. However, the /v1 and /v2 modules show how to manage even the GET handlers using custom PL/SQL when that level of flexibility might be useful to your application.

14.8 Layering Packages for Modularity#

Layer PL/SQL packages so pages and REST APIs share checks while handlers stay simple.

Layer your PL/SQL packages with care. This keeps ORDS handlers simple and lets pages and REST APIs run the same checks, no matter who sends the data.

14.9 Simplifying REST APIs in Oracle 26ai#

Simplify REST API development in Oracle 26ai with JSON type and duality views.

If you use Oracle 26ai, creating REST APIs for your applications is dramatically easier thanks to significant new JSON capabilities in this release.

14.10 Securing APIs with Role-Based Access Control#

Secure REST APIs with roles and OAuth 2.0 clients for authorized partner access.

For production application integration use, secure any REST APIs you create with role-based access control. Then, issue any partner application a secure OAuth 2.0 client for authenticated access to the APIs you authorize them to use.

14.1.1 Exploring Action Item Teams Example#

Explore an action items app that manages teams and enforces team membership rules.

Your application manages action items, staff members, and the team assigned to work on an action item. Each staff member on a particular action item team plays the role of lead or a regular team member. Your app enforces business rules like:
  • Every action item team needs a lead,
  • There can only be one lead on a team, and
  • No duplicate team members allowed.

The figure below shows a page from your app that all staff use daily. It shows a two-tab navigation structure with a tab to edit Staff members and a tab to edit Action Items. On the Action Items tab, the user can list, create, edit, and delete action teams of staff members assigned to work on an action item. The Website Redesign Project being edited in the figure has team members Levi, Opal, David, and Georgia. Levi's role is Lead, and the others' role is Member.

Figure 14-1 Application Page for Editing an Action Item and Assigned Team Members

The simple underlying data model appears below. Just three tables: ACTION_ITEMS, STAFF, and ACTION_ITEM_TEAM_MEMBERS. The entity relationship diagram shows that an action item has one or more team members, each of whom is a staff member.

Figure 14-2 Simple Schema for Action Items and Team Members Assigned to Them

14.1.2 Understanding the Integration Scenario#

Understand how an external ticketing system uses REST APIs to manage action items in your APEX app.

Your support team uses an open-source ticketing system outside your APEX environment. When a customer escalates a ticket that requires development work, that system uses a REST API to create an action item in your app. It assigns the support agent as the lead and adds key engineers as team members. If the ticket changes, the system updates the action item. If the ticket closes without requiring development work, the system deletes it.

The diagram illustrates the REST API interaction between the external ticketing system and the action item system built with APEX.

Figure 14-3 Open Source Ticketing System Needs to Integrate with Your Action Item App

14.2.1 Reviewing HTTP Methods for REST APIs#

Review how HTTP methods map to REST operations on business entities.

The Hypertext Transfer Protocol (HTTP) defines methods that state a client's purpose in requesting a server URL. In RESTful web services, the four methods below map to the Create, Read, Update, and Delete (CRUD) operations on resources. Each resource is a business entity like an Action Item.

Table 14-1 HTTP Methods and REST Operations

Scroll horizontally to view the full table
Method Business Entity Operation
POST Create a new entity
GET Read an entity collection or a specific entity
PUT Update an existing entity with a new version
DELETE Delete an entity

Tip:

PATCH is a less frequently used method for performing a partial update to a business entity. Services supporting the PATCH "verb" interpret the client's JSON payload as an incremental change to apply to the existing object.

14.2.2 Inspecting JSON Syntax#

Review JSON objects, arrays, and value types used in REST API payloads.

JavaScript Object Notation (JSON) is a lightweight, text-based format for representing structured data. It's easy for humans to read and write, and simple for programs to parse and generate. JSON describes data using two main structures:
  • Objects – collections of name/value pairs, enclosed in curly braces {}, and
  • Arrays – ordered lists of values, enclosed in square brackets [].
A JSON object begins with { and ends with }. Inside, one or more name/value pairs appear, separated by commas:
  • You always write names – also called properties or keys – in double quotes "…".
  • A value follows each name, separated by a colon.

For example, the object below represents a team member named Georgia. The keys are name, role, user_id, and active. The values "Georgia" and "LEAD" are strings, while 5 is a number and true is a boolean (true or false) value.

{
  "name": "Georgia",
  "role": "LEAD",
  "user_id": 5,
  "active": true
}

The order of the properties in a JSON object is not significant, so the following object is the same logical object as above, even though their two text representations are not exactly equal.

{
  "user_id": 5,
  "role": "LEAD",
  "name": "Georgia",
  "active": true
}
A JSON array starts with [ and ends with ]. Its elements can be strings, numbers, booleans, null, objects, or other arrays. Commas separate multiple elements. For example, an array of strings looks like:
["apple","banana","cherry"]

Tip:

In contrast to the ordering of object properties, array element order is significant. However, some apps may ignore that ordering.

Arrays often hold objects. For example, this is an array of two team members:
[
  { "name": "Georgia", "role": "LEAD",   "user_id": 5, "active": true},
  { "name": "Karl",    "role": "MEMBER", "user_id": 8, "active": true}
]

In JSON, a value can be one of the types below.

Table 14-2 JSON Types and Their Representations

Scroll horizontally to view the full table
Type Textual Representation
String "some text"
Number 42 or 3.14 (no quotes)
Boolean true or false
Null null
Object {…}
Array […]

For better readability, use any spaces, tabs, or line breaks you need. Parsers ignore this whitespace.

14.2.3 Constructing JSON in SQL#

Use JSON_OBJECT and JSON_ARRAYAGG SQL functions to construct JSON from relational data.

For example, the following query retrieves one JSON object per ACTION_ITEMS row. Notice you can include a column as is, with a default property name, or explicitly specify a column's property name using the value keyword. Study the team member name, user_id, and role. For consistency, it's fine to use value even if you choose the same lowercase version of the column name that would have been the default.

select json_object(
          '_id' value ai.id,
          ai.name,
          ai.status,
          'team' value (select json_arrayagg(
                                  json_object(
                                     'team_member_id' value tm.id,
                                     'name'           value s.name,
                                     'user_id'        value tm.user_id,
                                     'role'           value tm.role))
                          from action_item_team_members tm
                          join staff s
                            on tm.user_id = s.id
                         where tm.action_id = ai.id))
  from action_items ai
Each row of the result, looks like the following:
{
  "_id": 14,
  "name": "End of Year Party",
  "status: "OPEN",
  "team": [
    {
      "team_member_id": 23,
      "name": "David",
      "user_id": 4,
      "role": "MEMBER"
    },
    {
      "team_member_id": 24,
      "name": "Georgia",
      "user_id": 5,
      "role": "LEAD"
    },
    {
      "team_member_id": 25,
      "name": "Jane",
      "user_id": 13,
      "role": "MEMBER"
    }
  ]
}
When using JSON_ARRAYAGG to collect one or more elements into an array, you can include an ORDER BY clause to sort the array elements. For example, to sort the team members first by role, then by name, you can write:
select json_object(
          '_id' value ai.id,
          ai.name,
          ai.status,
          'team' value (select json_arrayagg(
                                  json_object(
                                     'team_member_id' value tm.id,
                                     'name'           value s.name,
                                     'user_id'        value tm.user_id,
                                     'role'           value tm.role)
                                  order by tm.role, s.name)
                          from action_item_team_members tm
                          join staff s
                            on tm.user_id = s.id
                         where tm.action_id = ai.id))
  from action_items ai
With this query, the equivalent row in the result shown above looks like this instead:
{
  "_id": 14,
  "name": "End of Year Party",
  "status": "OPEN",
  "team": [
    {
      "team_member_id": 24,
      "name": "Georgia",
      "user_id": 5,
      "role": "LEAD"
    },
    {
      "team_member_id": 23,
      "name": "David",
      "user_id": 4,
      "role": "MEMBER"
    },
    {
      "team_member_id": 25,
      "name": "Jane",
      "user_id": 13,
      "role": "MEMBER"
    }
  ]
}

14.2.4 Working with JSON in PL/SQL#

Use PL/SQL JSON types to parse, build, and serialize JSON data.

14.3.1 Choosing a Schema Alias#

Choose a schema alias to define the leading segment of your ORDS service URLs.

When you enable your schema to start creating APIs, you configure a schema alias to set the name that appears in your service URLs. For example, if your schema were named WKSP_COMPANION, you might choose cloudcompanion as its schema alias. When clients call your web apis, the URL begins with:
https://example.com/ords/cloudcompanion/...

14.3.2 Identifying URL Templates in a Module#

Define URL templates in a module to identify the service endpoints clients call.

You specify service endpoints using templates to identify the app-specific part of the URL a client uses. Then, you configure a handler for one or more HTTP methods for each template. Before you define templates, you first create a module to contain them. You can name the module anything you like, but say you call it v0.

Then, in your v0 module, you can use URL templates like:
  • /actionitems – for working with action items in general, and
  • /actionitems/:id – to work with a specific action item by unique identifier.
The URL a client app uses to call your web API starts with your domain, followed first by /ords/schemaalias, then by your module name and template. For example, to work with action items in general, the remote application uses the URL:
https://example.com/ords/cloudcompanion/v0/actionitems
To work instead with the specific action item with ID 1234, the client uses this URL instead:
https://example.com/ords/cloudcompanion/v0/actionitems/1234

14.3.3 Choosing GET Handlers to Read Data#

Use GET handlers to expose read-only access for an entity collection and a single entity.

Start by assuming your integration requires read-only access to action items. The HTTP GET method is the appropriate one to handle this case. The figure below shows the v0 ORDS module, with its two URL path templates, and a GET method handler for each template.

Figure 14-4 ORDS "v0" Module with Templates and Handlers for Action Items API

14.3.4 Creating a View for GET Handlers#

Create a view that returns action item data with nested team members for GET handlers.

Start by defining a view the /actionitems template's GET handlers can use. This gives a meaningful name like ACTION_ITEMS_WITH_TEAM_JV to the query that retrieves the exact action item data you want the handler to return. Select top-level ACTION_ITEMS columns you want to include, then use a subquery with JSON_ARRAYAGG and JSON_OBJECT as shown below to include the JSON array of nested ACTION_ITEM_TEAM_MEMBERS data. Notice how the subquery joins this table with STAFF to include the staff member's name in the result. Your ORDS GET handlers can now reference this query to simplify their definition.

create view action_items_with_team_jv as
select ai.id,
       ai.name,
       ai.status,
       (select json_arrayagg(
                 json_object(
                   'team_member_id' value tm.id,
                   'name'           value s.name,
                   'user_id'        value tm.user_id,
                   'role'           value tm.role
                 )
               )
          from action_item_team_members tm
          join staff s on tm.user_id = s.id
         where tm.action_id = ai.id
       ) as team
from action_items ai

14.4.1 Accessing SQL Developer Web REST Designer#

Open SQL Developer Web from App Builder to use its REST API designer.

After enabling your workspace schema for web API development, as shown below, launch SQL Developer Web any time from the SQL Workshop menu.

Tip:

If nothing happens, your browser may be blocking pop-up windows. After allowing them for your APEX instance's domain, repeat the menu selection to try again.

Figure 14-5 Opening SQL Developer Web from the App Builder

Caution:

If the SQL Developer Web entry is missing from your SQL Workshop menu, ask your APEX instance administrator to enable SQL Developer Web access in Administration Services.

When launched from App Builder, SQL Developer Web is connected to your workspace schema. As shown below, access the REST API designer using the REST entry on the "hamburger" menu.

Figure 14-6 Navigating to View and Define REST Services in SQL Developer Web

14.4.2 Navigating ORDS Service Definitions#

Navigate from modules to templates to handlers in SQL Developer Web REST Designer.

When you access SQL Developer Web's REST Designer, as shown below, you start on the landing page. The dashboard at the top shows the count of your modules and other information. Click on the Modules tile to see the module list.

Figure 14-7 Navigating to View and Define REST Modules in SQL Developer Web

The Modules list displays a tile for each, with summary information. From here you can create a new module, or use the three-vertical-dots menu as shown below to access an existing module's list of Templates.

Figure 14-8 Navigating to View and Configure a Module's Templates in SQL Developer Web

The Templates list displays a tile for each, with the service URL and a button to quickly copy it to the clipboard. From here, you can create a new template or use the three-vertical-dots menu as shown below to access an existing template's list of Handlers.

Figure 14-9 Navigating to View and Configure a Template's Handlers in SQL Developer Web

The Handlers list displays a tile for each. Use the three-vertical-dots menu as shown below to view or edit the handler's details. Also notice the "breadcrumb" bar with a hierarchy of links you can click to return to the v0 module's list of templates, the Modules list, or the REST designer's landing page.

Figure 14-10 Listing Handlers for a Template in SQL Developer Web

14.5.1 Defining a Read-Only Collection Query#

Define a Collection Query GET handler that returns action items with nested team data.

Define the ORDS template handler for the GET method on the /actionitems template using the Source Type of Collection Query. This handler type uses a SQL query to specify the data to return. Having already created the ACTION_ITEMS_WITH_TEAM_JV view, the SQL is very simple as shown below. Just select the top-level attributes you want to include in the result. Notice two column aliases in use. They affect the JSON response:
  • "_id" – ensures the ID attribute has the JSON property name _id
  • "{}team" – Uses {} to indicate the team property is already in JSON format
select id as "_id",
       name,
       status,
       team as "{}team"
  from action_items_with_team_jv
order by name, id

Once defined, your collection query handler for the v0 module's /actionitems template is ready to test out. Click the Copy to Clipboard button as shown below to paste its URL into your favorite testing tool.

Figure 14-11 Defining a Collection Query GET Handler for /v0/actionitems Template

14.5.2 Testing Handler with curl or Postman#

Test a GET endpoint with curl and jq or with Postman to inspect the JSON response.

Test your endpoint from the command line using curl. By default this utility uses the HTTP GET method, so the command is very simple:
$curl https://example.com/ords/cloudcompanion/v0/actionitems

JSON parsers ignore whitespace, so as shown below, ORDS returns data without extra spaces or newlines. On your terminal, the JSON wraps across many lines.

Figure 14-12 Testing ORDS GET Handler on the Command Line with curl

Pipe curl output into jq using | to format it. It indents and color-codes the result for easier reading.

$ curl https://example.com/ords/cloudcompanion/v0/actionitems | jq
Figure 14-13 Formatting Output of curl Using jq Command Line Utility

Postman is another popular tool developers use for experimenting with web APIs. As shown below, pick the HTTP method and enter the URL, then click (Send) to see the nicely-formatted response.

Figure 14-14 Using Postman to Access Your New Action Items REST API

14.5.3 Filtering/Paging Collection Query Endpoints#

Use query string parameters to filter and page through Collection Query results.

ORDS Collection Query GET handlers support query filtering using the q query string parameter, whose value is an ORDS JSON filter predicate. For example, testing a GET request with the following URL finds action items whose name contains the word Party:
$ curl https://example.com/⋯/v0/actionitems?q={"name":{"$like":"%Party%"}}

The response looks like the following if just one action item in the system matches.

{
    "items": [
        {
            "_id": 14,
            "name": "End of Year Party",
            "status": "OPEN",
            "team": [
                {
                    "team_member_id": 23,
                    "name": "David",
                    "user_id": 4,
                    "role": "MEMBER"
                },
                {
                    "team_member_id": 24,
                    "name": "Georgia",
                    "user_id": 5,
                    "role": "LEAD"
                },
                {
                    "team_member_id": 25,
                    "name": "Jane",
                    "user_id": 13,
                    "role": "MEMBER"
                }
            ]
        }
    ],
    "hasMore": false,
    "limit": 25,
    "offset": 0,
    "count": 1,
    "links": [
      ⋰
    ]
}
Notice the following properties in the JSON response:
{
       ⋮
    "hasMore": false,
    "limit": 25,
    "offset": 0,
    "count": 1,
       ⋮
}
An ORDS Collection Query handler's response includes the following top-level properties:
  • count – rows in this response "pageful"
  • limit – maximum rows in a single response, also called the "page size"
  • offset – rows that were skipped before returning the current "page"
  • hasMoretrue if there are more rows to retrieve, false otherwise
ORDS Collection Query GET handlers support paging through data by adding the query string parameters:
  • limit – maximum rows in a single response, also called the "page size"
  • offset – rows that were skipped before returning the current "page"
The default value of offset is 0 if not specified, so to fetch the first 10 actions items, use:
$ curl https://example.com/⋯/v0/actionitems?limit=10
If the response includes a hasMore value of true, then get the second page of 10 action items with:
$ curl https://example.com/⋯/v0/actionitems?offset=10&limit=10

Tip:

When configuring Collection Query GET handlers, always include an ORDER BY clause in your query to ensure a predictable result when paging.

You can combine filtering with paging, too. For example, to retrieve the first 5 action items whose name starts with Feature:, use:
$ curl https://⋯/v0/actionitems?limit=5&q={"name":{"$like":"Feature:%"}}
If the response includes a hasMore value of true, then get the second page of 5 action items with:
$ curl https://⋯/v0/actionitems?offset=5&limit=5&q={"name":{"$like":"Feature:%"}}

14.5.4 Configuring a Collection Item Handler#

Define a Collection Item GET handler that returns one action item, given its unique id.

Define the ORDS template handler for the GET method on the /actionitems/:id template using the Source Type of Collection Item. This handler type's SQL query returns a single row by referencing a value provided in the URL. The :id in the template defines an implicit variable. As shown below, your query uses its value with bind variable notation. Then, select the top-level attributes you want to include in the result.

select id as "_id",
       name,
       status,
       team as "{}team"
  from action_items_with_team_jv
 where id = :id

Tip:

Notice two column aliases in use. They affect the JSON response:

  • "_id" – ensures the ID attribute has the JSON property name _id
  • "{}team" – Uses {} to indicate the team property is already in JSON format

Once defined, your collection item handler for the v0 module's /actionitems/:id template is ready to test out. Click the Copy to Clipboard button as shown below to paste its URL into your favorite testing tool.

Figure 14-15 Defining an Collection Item GET Handler for /v0/actionitems/:id Template
For example, using curl to request action item number 14 produces the response below. Since the result is a single action item, notice the response has:
  • No items array
  • No pagination properties: count, hasMore, limit, or offset.
$ curl https://example.com/ords/cloudcompanion/v0/actionitems/14 | jq
{
    "id": 14,
    "name": "End of Year Party",
    "status": "OPEN",
    "team": [
        {
            "team_member_id": 23,
            "name": "David",
            "user_id": 4,
            "role": "MEMBER"
        },
        {
            "team_member_id": 24,
            "name": "Georgia",
            "user_id": 5,
            "role": "LEAD"
        },
        {
            "team_member_id": 25,
            "name": "Jane",
            "user_id": 13,
            "role": "MEMBER"
        }
    ],
    "links": [
        {
            "rel": "collection",
            "href": "https://example.com/ords/cloudcompanion/v0/actionitems/"
        }
    ]
}

14.6.1 Reviewing Rules for Data Changes#

Review the defaulting and validation rules the example app enforces for action item teams.

For any action item and its related action item team, your application enforces the following business rules:
  • If no role is specified, default it to MEMBER
  • Team member role on an action item team must be either LEAD or MEMBER
  • Each action item team must have a lead
  • Only one lead is allowed per action item team
  • No duplicate staff members are allowed on the same team.

14.6.2 Defining Text Messages for Errors#

Define Text Messages for translatable errors your pages and PL/SQL can share.

To make user-visible error messages in your business logic translatable, create shared component Text Messages for each one. As shown below, each has a:
  • Static ID (e.g. TEAM_MEMBER_NOT_FOUND),
  • language code (e.g. en), and
  • text that can include named substitution tokens (e.g. %name).
Figure 14-16 Shared Component Text Messages Define Translatable Error Messages
Your app pages and process messages access a string using this substitution syntax:
&{TEAM_MEMBER_NOT_FOUND}.

In PL/SQL code, GET_MESSAGE in the APEX_LANG package gets a string by its static ID in the user's language. If the message includes named tokens, pass an APEX_T_VARCHAR2 string list with each token name followed by its string value.

l_message := apex_lang.get_message(
                p_name   => 'TEAM_MEMBER_NOT_FOUND',
                p_params => apex_t_varchar2('name',l_team_member.name));

14.6.3 Using Text Messages in Pages and PL/SQL#

Use Text Message substitution syntax in pages and APEX_LANG.GET_MESSAGE in PL/SQL.

Your app pages and process messages access a string with its Statid ID using this substitution syntax:
&{TEAM_MAX_ONE_LEAD}.
If the message includes named tokens, then provide their values like this:
&{TEAM_MAX_ONE_LEAD name="&P3_NAME."}.

In PL/SQL code, GET_MESSAGE in the APEX_LANG package gets a string by its unique name. If the message includes named tokens, pass an APEX_T_VARCHAR2 string list with each token name followed by its string value.

l_message := apex_lang.get_message(
                p_name   => 'TEAM_MEMBER_NOT_FOUND',
                p_params => apex_t_varchar2('name',l_team_member.name));

14.6.4 Handling Errors in a Common Way#

Centralize error lookup, reporting, and logging so pages and APIs handle errors consistently.

Use a package like APP_COMMON below to centralize application-wide common error handling logic. This simplifies reporting and logging errors from a single location. Key functions include:
  • error_message – to return an error message in the current language based on a Static ID
  • report_if_error – to optionally raise an error if the message passed in is not null
  • report_error_key – to report an error in the current language based on a Static ID
package app_common is

    c_workspace constant varchar2(40) := 'COMPANION';
    c_app_id    constant number       := 1001;

    -----------------------------------------------------------
    -- Return error message with indicated key
    -----------------------------------------------------------
    function error_message(
        p_message_key in varchar2,
        p_params      in apex_t_varchar2 default null)
        return           varchar2;
    -----------------------------------------------------------
    -- Report error message with indicated behavior if non-null
    -----------------------------------------------------------
    procedure report_if_error(
        p_error_message in varchar2);
    -----------------------------------------------------------
    -- Report error message with indicated message key
    -----------------------------------------------------------
    procedure report_error_key(
        p_error_message_key in varchar2,
        p_params            in apex_t_varchar2 default null);
end app_common;

The APP_COMMON package body appears below. Notice its is_apex_call that detects whether the error is being reported while running in the context of an APEX application or not. The report_if_error procedure uses this to decide whether to add the error to the APEX error stack using APEX_ERROR.ADD_ERROR, or whether to raise an exception containing the error message instead. The code uses APEX_LANG.GET_MESSAGE to get the error message by Static ID in the user's current language.

package body app_common is
    --------------------------------------------
    function is_apex_call
    return boolean
    is
    begin
        return apex_application.g_flow_id is not null;
    end is_apex_call;
    --------------------------------------------
    function error_message(
        p_message_key in varchar2,
        p_params      in apex_t_varchar2 default null)
        return           varchar2
    is
    begin
        apex_util.set_workspace(c_workspace);
        return apex_lang.get_message(
                p_name           => p_message_key,
                p_params         => p_params,
                p_application_id => c_app_id);
    end error_message;
    --------------------------------------------
    procedure report_if_error(
        p_error_message in varchar2)
    is
        l_error varchar2(4000);
    begin
        if p_error_message is not null then
            if is_apex_call then
                apex_error.add_error (
                    p_message          => p_error_message,
                    p_display_location => apex_error.c_on_error_page);
            else
                raise_application_error(-20001,p_error_message);
            end if;
        end if;
    end report_if_error;
    -----------------------------------------------
    procedure report_error_key(
        p_error_message_key in varchar2,
        p_params            in apex_t_varchar2 default null)
    is
    begin
        report_if_error(
            p_error_message => error_message(p_error_message_key,p_params));
    end report_error_key;
end app_common;

14.6.5 Coding Business Rules for Reuse#

Put shared business rules in a package so APEX pages and REST APIs can reuse the same logic.

To reuse common action item business logic in your app and REST APIs, put it in a separate package. For example, ACTION_ITEMS_COMMON below has validation functions for action items. They return either a:
  • BOOLEAN – with false representing invalid, or
  • VARCHAR2 – with null representing success, or an error message on failure.
The validation checks include functions:
  • action_exists – checks whether an action items with a given ID exists
  • ensure_null_id_for_new_action – asserts a new action item's ID is null
  • ensure_action_exists – returns message if action does not exist
  • one_lead_per_action_team – returns message if action item team has no leads or more than one lead
  • no_duplicate_team_members – returns message if duplicate team members exists
package action_items_common is
    -----------------------------------------------------------------------
    -- Return true if the action item with provide id exists
    -----------------------------------------------------------------------
    function action_exists(
        p_action_id in number)
    return boolean;
    -----------------------------------------------------------------------
    -- Ensure id passed in is null, or return error message
    -----------------------------------------------------------------------
    function ensure_null_id_for_new_action(
        p_id   in number)
        return    varchar2;
    -----------------------------------------------------------------------
    -- Ensure action item exists, or return error message
    -----------------------------------------------------------------------
    function ensure_action_exists(
        p_action_id in number)
    return varchar2;
    -----------------------------------------------------------------------
    -- Ensure action item has exactly one Lead, or return error message
    -----------------------------------------------------------------------
    function one_lead_per_action_team(
        p_action_id in number)
    return varchar2;
    -----------------------------------------------------------------------
    -- Ensure action item has no duplicate members, or return error message
    -----------------------------------------------------------------------
    function no_duplicate_team_members(
        p_action_id in number)
    return varchar2;
    -----------------------------------------------------------------------
    -- Ensure role value is legal, or return error message
    -----------------------------------------------------------------------
    function is_valid_role(
        p_role_value in varchar2)
        return          varchar2;
end action_items_common;
You can study the simple code that implements the variable functions and procedures below. Notice how validations can easily enforce conditions about the state of a parent Action Item and its child Action Item Team Members using simple SQL statements. Two key examples to review:
  • To count team leads, one_lead_per_action_team uses:

    COUNT(*) with WHERE ROLE='LEAD'

  • To find duplicate team members, no_duplicate_team_members uses:

    GROUP BY and HAVING COUNT(*) > 1

Tip:

These kinds of simple SQL-based aggregate checks are possible because both your APEX application and REST API PL/SQL can perform checks after the data changes are saved but before they are committed. Signaling an error in either context is enough to rollback the transaction and let the user know the problem with a helpful error message in their current language.

package body action_items_common is
    ------------------------------------------------------------
    -- Constant for error message keys
    ------------------------------------------------------------
    c_msg_team_must_have_lead   constant varchar2(25) := 'TEAM_MUST_HAVE_A_LEAD';
    c_msg_team_max_one_lead     constant varchar2(25) := 'TEAM_MAX_ONE_LEAD';
    c_msg_team_no_duplicates    constant varchar2(25) := 'TEAM_NO_DUPE_MEMBERS';
    c_msg_action_item_not_found constant varchar2(25) := 'ACTION_ITEM_NOT_FOUND';
    c_msg_illegal_role_value    constant varchar2(25) := 'INVALID_ROLE_TYPE';
    c_msg_no_id_for_new_action  constant varchar2(25) := 'ID_UNEXPECTED_FOR_INSERT';

    c_no_error                  constant varchar2(1)  := null;
    ------------------------------------
    function ensure_null_id_for_new_action(
        p_id   in number)
        return    varchar2
    is
    begin
        if p_id is not null then
            return app_common.error_message(c_msg_no_id_for_new_action);
        end if;
        return c_no_error;
    end ensure_null_id_for_new_action;
    ------------------------------------
    function ensure_action_exists(
        p_action_id in number)
    return varchar2
    is
    begin
        if not action_exists(p_action_id) then
            return app_common.error_message(c_msg_action_item_not_found,
                    apex_t_varchar2('id',p_action_id));
        end if;
        return c_no_error;
    end ensure_action_exists;
    ------------------------------------
    function action_exists(
        p_action_id in number)
    return boolean
    is
    begin
        for j in (select null
                    from action_items
                   where id = p_action_id)
        loop
            return true;
        end loop;
        return false;
    end action_exists;
    ------------------------------------
    function one_lead_per_action_team(
        p_action_id in number)
        return         varchar2
    is
        l_lead_count number;
    begin
        -- Ensure the action id still exists since it might
        -- have been deleted. If it exists, perform the check
        -- on the children rows.
        for j in (select id
                    from action_items
                   where id = p_action_id) loop
            select count(*)
            into l_lead_count
            from action_item_team_members
            where action_id = p_action_id
            and role = 'LEAD';
            if l_lead_count != 1 then
                return app_common.error_message(
                      case
                            when l_lead_count < 1
                            then c_msg_team_must_have_lead
                            else c_msg_team_max_one_lead
                        end);
            end if;
        end loop;
        return c_no_error;
    end;

    ------------------------------------
    function no_duplicate_team_members(
        p_action_id in number)
        return         varchar2
    is
    begin
        -- Ensure the action id still exists since it might
        -- have been deleted. If it exists, perform the check
        -- on the children rows.
        for j in (select id
                    from action_items
                   where id = p_action_id) loop
            for k in (select user_id, count(*)
                        from action_item_team_members
                       where action_id = p_action_id
                       group by user_id
                       having count(*) > 1
                       fetch first row only) loop
                return app_common.error_message(c_msg_team_no_duplicates);
                exit;
            end loop;
        end loop;
        return c_no_error;
    end;

    ------------------------------------
    function is_valid_role(
        p_role_value in varchar2)
        return          varchar2
    is
    begin
        if p_role_value not in ('MEMBER','LEAD') then
            return app_common.error_message(
                    c_msg_illegal_role_value,
                    apex_t_varchar2('value',p_role_value));
        end if;
        return c_no_error;
    end is_valid_role;
end action_items_common;

14.6.6 Experiencing Validation Failure in UI#

Show validation failures in the UI using the same shared business logic your REST APIs use.

After you set up the shared Action Item logic in the ACTION_ITEMS_COMMON package, app pages use it declaratively through Invoke API page processes or validations. The figure below shows what users see when saving an action item team with two leads. In the figure, the user clicked (Apply Changes) on the Action Item edit page for the Website Redesign Project. The Action Team grid below shows both Levi and Opal as having the role of Lead. The error notification area appears with an orange triangle containing an exclamation point and the title "1 error has occurred". The application specific error appears below that as a bulleted message: "Only one Lead per action team".

Figure 14-17 Editing an Action Item and Assigned Team Members in a Page

14.6.7 Receiving Validation Error in REST API#

REST API clients see the same validation errors that end users see in the app.

Your REST APIs URL template handlers call common Action Item logic in the ACTION_ITEMS_COMMON package. If a REST API client tries to create a new action item with duplicate team members, the call fails. The behavior is the same for curl, Postman, or another system.

As shown below, HTTP error 400 signals an application-specific failure, and the JSON response explains why. By using shared logic and error handling, REST clients see the same messages app users see for the same invalid data. The figure shows the result of using the Postman REST API testing tool to POST a request to the endpoint:

https://example.com/ords/cloudcompanion/v1/actionitems

The example request's body contains the following JSON payload where both Levi and Opal team members have the role of LEAD.

{
  "name": "Website Redesign Project",
  "team": [
     {"role":"LEAD",  "name":"Levi"},
     {"role":"LEAD",  "name":"Opal"},
     {"role":"MEMBER","name":"David"},
     {"role":"MEMBER","name":"Georgia"},
  ]
}
The status 400 response body includes the JSON payload:
{"$error":"Only one lead per action team"} 
Figure 14-18 Working with Action Items and Team Members Using Postman

14.7.1 Providing Complete Set of ORDS Handlers#

Understand the full set of operations your REST API can implement for maximum control.

When your integration requirements go beyond the read-only use cases, consider creating handlers for all five of the operations a client may need to perform using your business entity REST API. For example, the v1 module shown in the diagram below has handlers for:
  • Listing/Searching Action ItemsGET on /v1/actionitems
  • Creating a New Action ItemPOST on /v1/actionitems
  • Getting a Specific Action Item by IDGET on /v1/actionitems/:id
  • Updating a Specific Action Item by IDPUT on /v1/actionitems/:id
  • Deleting a Specific Action Item by IDDELETE on /v1/actionitems/:id
Figure 14-19 ORDS "v1" Module with Templates and Handlers for Action Items API

14.7.2 Using Handler Bind Variables#

Use handler bind variables to read URL values, request bodies, parameters, and headers.

Your handler can use bind variables to access any of the following information in the incoming request:

  • URL path variables – like :id from /v1/actionitems/:id template
  • Implicit parameters like:
    • Request body – using :body (BLOB) or :body_text (CLOB)
  • Handler parameters you configure explicitly:
    • Query string parameters – e.g. /v1/actionitems?skip=5&pagesize=5
    • HTTP Headers – e.g. :debug to reference a header named app$debug
    • Request payload properties

Caution:

Your handler can only reference either the :body or :body_text variable, and you can reference the one you pick only a single time. Any attempt beyond the first one to read a body variable returns null. So, if you need to reference either one in multiple places, assign the bind variable to a local PL/SQL variable first, then reference the variable anywhere you need to.

14.7.3 Keeping PL/SQL Handlers Maintainable#

Keep ORDS PL/SQL handlers small by delegating REST logic to package procedures.

To simplify maintenance, organize your REST API code in PL/SQL packages. This lets each PL/SQL handler make a one-line call to a package procedure. The figure below shows the PUT handler for the template /actionitems/:id template, which updates a specific action item by ID:
begin
   action_items_api_rest.update_object(:id,:body_text,:debug);
end;
Notice the one-line PL/SQL block references three parameters:
  • :id – from the URL pattern parameter in the /actionitems/:id template
  • :body_text – implicit parameter to pass the JSON document CLOB the client sends
  • :debug – explicitly defined parameter for the HTTP header named app$debug

The figure shows the v1 ORDS module's actionsitems/:id template's PUT handler PL/SQL block explained above in context in the SQL Developer Web REST Designer. Notice the :debug listed in the Handler Parameters section and :body_text in the Implicit Parameters sections on the right.

Figure 14-20 An ORDS Template Handler Can Use PL/SQL for Complete Control

14.7.4 Defining Explicit Handler Parameters#

Define explicit handler parameters for request headers, query strings, and other inputs.

The figure below shows the Parameters tab of the PL/SQL PUT handler's details page. Notice how it defines the bind variable name debug for an incoming HTTP Header named app$debug. Use this tab to create any explicit parameters your handler needs, defining a parameter name, bind variable name, source type, access method, and data type for each one.

Figure 14-21 Defining a Custom Parameter for an ORDS Handler

14.7.5 Reusing Common REST API Handler Code#

Reuse common REST handler code to send JSON responses, errors, and optional debug details.

Every PL/SQL handler performs the following set of steps:
  • If necessary:
    • processes incoming request by referencing bind variables
    • prepares response to return to the client
  • Then always:
    • Sets the MIME Type of the response, and any other response headers
    • Sets the status code if the default 200 status is not appropriate
    • Closes the header
    • Writes the response to the output buffer using HTP.P or HTP.PRN
To simplify handler implementation, consider using a package like APP_COMMON_REST below that provides procedures for:
  • commit_and_send_response – committing the transaction and writing out the response
  • handle_error – rolling back the transaction, signalling an error, and returning error details
  • set_debug – to enable including additional debug info useful to developers.
package app_common_rest as
    --------------------------------------------------------------
    -- Set debug mode for error reporting
    --------------------------------------------------------------
    procedure set_debug(
        p_value in varchar2);
    --------------------------------------------------------------
    -- Commit and write out JSON response
    --------------------------------------------------------------
    procedure commit_and_send_response(
        p_json json_element_t default null);
    --------------------------------------------------------------
    -- Handle error by sending HTTP error with $error payload
    --------------------------------------------------------------
    procedure handle_error(
        p_error_message   in varchar2,
        p_http_error_code in number default 400);
end app_common_rest;
The package body follows for you to study. Notice it uses:
  • owa_util.mime_header – to set standard Content-Type MIME type header
  • owa_util.status_line – to set a non-default HTTP response status code
  • htp.p – to write out the response body as serialized JSON text.

If the g_debug_mode flag is enable by a call to set_debug, then notice also that the handle_error adds an additional debug$error property whose value is a JSON array containing the lines of the PL/SQL call stack info from format_error_stack and format_error_backtrace in the dbms_utility package.

create or replace package body app_common_rest as
    g_debug_mode boolean := false;
    ------------------------------------
    procedure set_debug(
        p_value in varchar2)
    is
    begin
        if upper(p_value) = 'TRUE' then
            g_debug_mode := true;
        end if;
    end set_debug;
    ------------------------------------
    procedure commit_and_send_response(
        p_json in json_element_t default null)
    is
        l_has_json boolean := p_json is not null;
    begin
        commit;
        owa_util.mime_header('application/json', l_has_json);
        if p_json is not null then
            htp.p(p_json.stringify);
        else
            owa_util.status_line(204, 'No content', true);
        end if;
    end;
    --------------------------------------------------------------
    -- Handle error by sending HTTP 400 with $error payload
    --------------------------------------------------------------
    procedure handle_error(
        p_error_message   in varchar2,
        p_http_error_code in number default 400)
    is
        l_json_err json_object_t := json_object_t();
        l_json_arr json_array_t;
    begin
        owa_util.mime_header('application/json', p_error_message is null);
        owa_util.status_line(p_http_error_code, 'Error', true);
        if p_error_message is not null then
            l_json_err.put('$error',
                           regexp_replace(p_error_message,'^ORA-[0-9]+:\s'));
            if g_debug_mode then
                l_json_arr := json_array_t();
                for j in (select rownum, column_value
                            from table(apex_string.split(
                                dbms_utility.format_error_stack()
                                || chr(10)
                                || dbms_utility.format_error_backtrace())))
                loop
                    if j.column_value is not null then
                        l_json_arr.append(j.column_value);
                    end if;
                end loop;
                l_json_err.put('debug$error', l_json_arr);
            end if;
            htp.p(l_json_err.stringify);
        end if;
        rollback;
    end;
end app_common_rest;

14.7.6 Reading and Writing Binary Content#

Read binary content with a Media Resource handler and store uploads using the :body bind variable.

When writing an ORDS handler that needs to return binary content, the simple way is to use a Media Resource type handler. Write a query with two columns that returns a single row. The first column provides the MIME type value, and the second column is the BLOB content to return. Imagine a template /attachments/:id in a module that needs to return an attachment by id. After choosing Media Resource handler type, write the query:
select mime_type, file_contents
  from attachments
 where id = :id
When writing PL/SQL handlers that accept a binary payload, use the :body implicit variable to reference it. For example, suppose you have a template /attachments and you write a POST handler to store an uploaded attachment. You can reference the implicit :content_type and :body parameters to insert the attachment like this:
begin
   your_package.insert_attachment(
       p_mime_type     => :content_type,
       p_file_contents => :body);
   commit;
end;

Caution:

Your handler can only reference the :body variable a single time. Any attempt beyond the first one to read it returns null. So, if you need to reference it in multiple places, assign the bind variable to a local PL/SQL variable first, then reference the variable anywhere you need to.

14.8.1 Studying API Handler Package Layers#

Study how ORDS handlers call a REST layer that delegates to shared core API logic.

The figure below illustrates how the five v1 module template handlers call appropriate procedures in the ACTIONS_ITEMS_API_REST package. Each handler uses a combination of ORDS implicit and explicit parameter bind variables to pass incoming request information to the corresponding package procedure. The digram shows the following ORDS handlers and the ACTION_ITEMS_API_REST procedure each invokes:
  • GET /v1/actionitemsget_objects
  • GET /v1/actionitems/:idget_object
  • POST /v1/actionitemsinsert_object
  • PUT /v1/actionitems/:idupdate_object
  • DELETE /v1/actionitems/:iddelete_object
Figure 14-22 Each ORDS Handler Call an ACTION_ITEMS_API_REST Procedure

The package specification for ACTION_ITEMS_API_REST appears below.

package action_items_api_rest as
    ------------------------------------
    procedure get_objects(
        p_search in varchar2,
        p_offset in number   default 0,
        p_limit  in number   default 10,
        p_debug  in varchar2 default null);
    ------------------------------------
    procedure get_object(
        p_id in number,
        p_debug  in varchar2 default null);
    ------------------------------------
    procedure insert_object(
        p_object in clob,
        p_debug  in varchar2 default null);
    ------------------------------------
    procedure update_object(
        p_id     in number,
        p_object in clob,
        p_debug  in varchar2 default null);
    ------------------------------------
    procedure delete_object(
        p_id     in number,
        p_debug  in varchar2 default null);
end action_items_api_rest;

As shown below, the ACTION_ITEMS_API_REST package is a REST-specific layer on top of the ACTIONS_ITEM_API package that contains the core of the API logic. Its simple procedures essentially convert the incoming JSON CLOB payloads into a JSON_OBJECT_T types to pass to the core Action Items API package. Then they use the common functionality in APP_COMMON_REST to write out the response or error details. This layering keeps the ORDS handler PL/SQL blocks extremely simple.

Notice in the diagram how the ACTION_ITEMS_API package reuses application-wide common code from APP_COMMON and Action Item specific common validation logic from ACTION_ITEMS_COMMON. This layering maximizes reuse so your application pages and REST APIs apply the same checks to modified data, regardless of how the users or external systems submit it.

The figure illustrates a package dependency diagram showing:
  • ACTION_ITEMS_API_REST uses
    • ACTION_ITEMS_API for logic
    • APP_COMMON_REST as a REST services helper
  • ACTION_ITEMS_API uses
    • ACTION_ITEMS_COMMON for validation
    • APP_COMMON for error handling
  • ACTION_ITEMS_COMMON also uses APP_COMMON for error handling
In addition, the diagram shows that the Action Items edit page uses the same packages:
  • ACTION_ITEMS_COMMON for validation
  • APP_COMMON for error handling
Figure 14-23 Layered Package Design for Sharing Business Logic with a Page

14.8.2 Examining Core Action Item API Logic#

The ACTION_ITEMS_API package is the heart of the Action Items REST API.

It implements the five public procedures the ORDS template handlers call via ACTION_ITEMS_API_REST:
  • get_object – to retrieve a single action item by ID
  • get_objects – to list or search action items with paging support
  • insert_object – to create a new action item
  • update_object – to update an existing action item by ID
  • delete_object – to delete an existing action item by ID.

Each procedure uses other interesting private helper routines whole roles are important to understand before digging into the full code of the package body.

14.9.1 Using JSON Type and Duality Views#

Use JSON type and duality views to work with relational data as JSON.

Oracle 26ai's JSON type further simplifies JSON data processing in SQL and PL/SQL. Its JSON Relational Duality Views let you define a declarative JSON structure for one or more related tables using a simplified syntax. This JSON view of the underlying relational data model can be read-only if desired, but you can also define it to support inserting, updating, and deleting data through the same view.

14.9.2 Exploring ORDS AutoREST for Duality View#

Expose a JSON relational duality view quickly by enabling ORDS AutoREST access.

Once you've created a JSON Relational Duality View, the fastest way to expose it to REST clients is to enable AutoREST access on it using SQL Developer Web.

14.9.3 Configuring ORDS Handlers for 26ai API#

Set up ORDS handlers to call an alternate API that uses 26ai JSON type and a duality view.

The v2 module has the same template and handlers as the v1 module. However, its handlers use 26ai JSON type and Duality Views to simplify the implementation. The diagram shows the module, its templates, and their handlers using the segment of the REST API URL they contribute. The v2 module has an /actionitems template with GET and POST handlers. Its /actionitems/:id template has GET, PUT, and DELETE handlers.

Figure 14-28 ORDS "v2" Module for Action Items API Using JSON Relational Duality View
Similar to the v1 module handlers, each v2 handler shown below calls the indicated procedure in the ACTION_ITEMS_DV_API_REST package:
  • GET /v2/actionitemsget_objects
  • GET /v2/actionitems/:idget_object
  • POST /v2/actionitemsinsert_object
  • PUT /v2/actionitems/:idupdate_object
  • DELETE /v2/actionitems/:iddelete_object
Figure 14-29 Each ORDS Handler Call an ACTION_ITEMS_API_REST Procedure

The ACTION_ITEMS_DV_API_REST accepts the CLOB JSON payload and constructs a JSON type to pass to the main ACTION_ITEMS_DV_API package. The diagram below shows the similar package layering the v1 module uses.

The figure illustrates a package dependency diagram showing:
  • ACTION_ITEMS_DV_API_REST uses
    • ACTION_ITEMS_API for logic
    • APP_COMMON_REST as a REST services helper
  • ACTION_ITEMS_DV_API uses
    • ACTION_ITEMS_COMMON for validation
    • APP_COMMON for error handling
  • ACTION_ITEMS_COMMON also uses APP_COMMON for error handling
In addition, the diagram shows that the Action Items edit page uses the same packages:
  • ACTION_ITEMS_COMMON for validation
  • APP_COMMON for error handling
Figure 14-30 Layered Package Design for Sharing Business Logic with a Page

14.9.4 Seeing How Duality View Simplifies Logic#

Compare the 26ai duality view API with the 19c version to see how much code it removes.

Using JSON type and a Duality View simplifies many aspects of the REST API implementation with custom business logic. For this action items REST API example, the 26ai version requires 51% less code to implement the same functionality:
  • 158 lines in ACTION_ITEMS_DV_API, versus
  • 325 lines in Oracle 19c-compatible ACTION_ITEMS_API.

14.10.1 Defining ORDS Role and Privileges#

Define ORDS roles and privileges to control access to protected REST API modules.

Define an ORDS Role in SQL Developer Web to name to a group of access privileges. Use the Security > Roles menu option from the SQL Developer Web REST Designer. For example, create a role named External Application Integration to which you can associate the set of privileges required for an external system to integrate with your Action Items application REST API endpoints.

Next, define a privilege to manage access to a specific set of modules or URL patterns. For example, you can create a privilege named com.example.ACTION_ITEMS_PRIVILEGE with display label Action Items Privilege. As shown below, you configure the privilege to protect the modules v0, v1, and v2 created in this section.

Figure 14-31 Defining a Privilege in SQL Developer Web REST Designer

To grant this privilege to the External Application Integration role created above, switch to the Roles tab and shuttle it into the Selected Roles list as shown below. If you create many different privileges to protect different modules or URL patterns, you can grant all the appropriate privileges in this same way to External Application Integration role.

Figure 14-32 Granting the Action Items Privilege to External Application Integration Role

14.10.2 Issuing OAuth Client for Authentication#

Issue an OAuth client so an external system can authenticate and access authorized APIs.

When an external app like the Support Ticketing System needs to access your REST APIs, issue it an OAuth 2.0 client. This acts as its "ticket" for secure access. ORDS generates a client id and secret, which you share securely. The third-party client uses these credentials to get a bearer token and includes it in the Authorization header of every REST API call. The role you assign to the OAuth client controls which APIs they can access, based on the privileges granted to that role. Create a distinct OAuth client for each external system that needs access.

Use the Security > OAuth Clients menu in the SQL Developer Web REST Designer to access the list of existing clients. Then, click (Create OAuth Client) to add a new one. Choose the Grant Type of client credentials (CLIENT_CRED), give it a name and description like Support Ticketing System, and enter a support email. On the Roles tab, associate the new client to External Application Integration role to grant it all privileges the role provides.

As shown below, when you finish the process by clicking (Create), ORDS assigns a random client id and client secret. You get one chance to see the client credential, so store it in your password vault for safe keeping. As shown below, the new OAuth client appears in the list.

Figure 14-33 Store Client Secret Securely, then Share Securely with External App Team

Your new client appears in the OAuth Clients list in SQL Developer Web's REST Designer as shown below. Options on the three-vertical-dots menu let you revoke the secret if needed, or rotate it should it get lost or you want to change it.

Figure 14-34 ORDS OAuth Client Used to Authenticate External System Integration

14.10.3 Using REST API with an OAuth Client#

Use an OAuth client to get a bearer token and call authorized REST APIs.

To use an OAuth client, the third party system starts by acquiring a bearer token using the client id and client secret provisioned for their integration. You can test the scenario on the command line using curl. Notice that the URL shares the same base as your REST APIs up to an including the schema alias (e.g. cloudcompanion). Then it ends with the /oauth/token path segments. The ‑‑data flag makes curl send a POST request with Content‑Type header set to application/x‑www‑form‑urlencoded and a request body containing grant_type=client_credentials. If the client id and secret are valid, the token endpoint returns a JSON response containing the bearer-type access token. It also includes the number of seconds that the token is valid. In the example below, the token expires in 3600 seconds, or 1 hour.

$ curl --user "QvUVx6Gp8P02dTb0C-II1A..:szUul--kEJ1Te-kS454_tQ.." \
--data "grant_type=client_credentials" \
https://example.com/ords/cloudcompanion/oauth/token

{"access_token":"SNVDe2Tjough44I_X6tmXg","token_type":"bearer","expires_in":3600}

Once the system gets its token, for an hour it can send that token in the Authorization header of any REST API request it makes to the APIs their OAuth client's role authorizes them to use. For example, using curl this access looks like this. Notice the word Bearer, followed by a space, precedes the token value in the header.

$ curl -H "Authorization: Bearer SNVDe2Tjough44I_X6tmXg"  \
https://example.com/ords/cloudcompanion/v1/actionitems/14
As long as the token is still valid, your REST API returns the expected response. After an hour, the external system can contact the token URL again to get a "fresh" token. If they fail to get a new token before it expires, their attempt to use an expired token will response with an HTTP 401 Unauthorized error, including the payload below:
{
    "code": "Unauthorized",
    "message": "Unauthorized",
    "type": "tag:oracle.com,2020:error/Unauthorized",
    "instance": "tag:oracle.com,2020:ecid/636fea296204b162106f0cdb5d7e22bc"
}

14.2.4.1 Parsing JSON in PL/SQL#

Parse JSON text into PL/SQL JSON objects and arrays, then access their values by type.

PL/SQL offers native types JSON_OBJECT_T and JSON_ARRAY_T for working programmatically with JSON data. Create an instance by passing JSON text in a VARCHAR2 or CLOB to the type constructor function. Then as shown below you can access property values using getter functions, depending on the data type expected, and access array elements using the get() function.

declare
    l_actionitem json_object_t;
    l_team       json_array_t;
    l_member     json_object_t;
    -----------------------------
    function get_object(
        p_array in json_array_t,
        p_index in pls_integer)
        return     json_object_t
    is
    begin
        return treat(p_array.get(p_index) as json_object_t);
    end get_object;
begin
   l_actionitem := json_object_t(q'~
                    {
                      "name": "Practice Using JSON in PL/SQL",
                      "team": [
                        {"role": "LEAD","name": "Georgia"},
                        {"name": "Usha","role": "MEMBER"}
                      ]
                    }
                    ~');
   l_team   := l_actionitem.get_array('team');
   l_member := treat(l_team.get(0) as json_object_t);
   -- All three print out "Georgia"
   dbms_output.put_line(l_member.get_string('name'));
   dbms_output.put_line(treat(l_team.get(0) as json_object_t).get_string('name'));
   dbms_output.put_line(get_object(l_team,0).get_string('name'));
end;

Tip:

When calling object functions on JSON types, ensure the variable is not null. Calling an object function on a null produces the error message:
ORA-30625: method dispatch on NULL SELF argument is disallowed

14.2.4.2 Treating an Element More Specifically#

Use TREAT to work with a generic JSON element as a more specific PL/SQL JSON type.

The generic type JSON_ELEMENT_T is the "grandparent" in the PL/SQL family of JSON types. A JSON array can hold scalars, objects, or other arrays. In the tree below, if type B is indented inside type A, then you can say B is a more specific kind of A.

JSON_ELEMENT_T
├── JSON_SCALAR_T
│   ├── JSON_NUMBER_T
│   ├── JSON_STRING_T
│   └── JSON_BOOLEAN_T
├── JSON_OBJECT_T
└── JSON_ARRAY_T
Since PL/SQL functions of the same name can't return different types, JSON_ARRAY_T.GET() returns the root JSON_ELEMENT_T type. To explicitly treat the returned array element as one of the more specific types, use:
TREAT(… AS JSON_SPECIFIC_TYPE)
A function like GET_OBJECT below can help save you typing TREAT a lot if you work frequently with arrays of objects.
-- Get object with index p_index in an array of objects
function get_object(
    p_array in json_array_t,
    p_index in pls_integer)
    return     json_object_t
is
begin
    return treat(p_array.get(p_index) as json_object_t);
end get_object;

14.2.4.3 Constructing JSON in PL/SQL#

Build JSON objects and arrays in PL/SQL by adding properties and array elements.

You can construct JSON programmatically property by property, starting with empty JSON objects and arrays. As shown below, notice that the code first builds the team member array's contents, then puts the array in the containing object with a team property name.

declare
    l_actionitem     json_object_t := json_object_t();
    l_team           json_array_t  := json_array_t();
    l_member_usha    json_object_t;
    l_member_georgia json_object_t;
begin
   l_actionitem.put('name', 'Practice Using JSON in PL/SQL');
   l_member_georgia := json_object_t();
   l_member_georgia.put('role', 'LEAD');
   l_member_georgia.put('name', 'Georgia');
   l_team.append(l_member_georgia);
   l_member_usha := json_object_t();
   l_member_usha.put('name', 'Usha');
   l_member_usha.put('role', 'MEMBER');
   l_team.append(l_member_usha);
   -- append all array elements before adding array to object
   l_actionitem.put('team', l_team);
   -- Both print out "Georgia"
   dbms_output.put_line(l_member_georgia.get_string('name'));
   dbms_output.put_line(treat(l_team.get(0) as json_object_t).get_string('name'));
end;

Tip:

When calling object functions on JSON types, ensure the variable is not null. Calling an object function on a null produces the error message:
ORA-30625: method dispatch on NULL SELF argument is disallowed

14.2.4.4 Serializing JSON in PL/SQL#

Serialize PL/SQL JSON objects to text with STRINGIFY or TO_CLOB, depending on size.

For a variable of type JSON_OBJECT_T, two object functions let you "serialize" its data to textual format:
  • STRINGIFY – when the text does not exceed 32767 characters, or
  • TO_CLOB – for a JSON object of any size.

The following example containing a JSON object with a property having a value of length 32767 shows how TO_CLOB works correctly. However, uncommenting the call to STRINGIFY produces an error since the JSON object's text representation exceeds 32767 characters.

declare
    l_value varchar2(32767) := lpad('y',32767,'x');
    l_json  json_object_t   := json_object_t();
    -------------------------------------------------------
    procedure p(p_clob in clob)
    is
        l_offset integer := 1;
        l_chunk  varchar2(32767);
    begin
        while l_offset <= dbms_lob.getlength(p_clob) loop
        l_chunk := dbms_lob.substr(p_clob, 32767, l_offset);
        dbms_output.put_line(l_chunk);
        l_offset := l_offset + 32767;
        end loop;
    end p;
begin
  l_json.put('name',l_value);
  -- Gives ORA-40478: output value too large
  -- p(l_json.stringify);
  p(l_json.to_clob);
end;

Tip:

When calling object functions on JSON types, ensure the variable is not null. Calling an object function on a null produces the error message:
ORA-30625: method dispatch on NULL SELF argument is disallowed

14.8.2.1 Constructing or Querying an Action Item#

Compare programmatic and SQL-based ways to build action item JSON.

For educational purposes, the GET_OBJECT procedure shows two different ways to return the JSON representation of an action item. As shown below, if the c_get_object_approach constant equals c_approach_programmatic then it calls GET_OBJECT_PROGRAMMATICALLY. If the constant is c_approach_sql instead, then it calls GET_OBJECT_USING_SQL.

-- in package action_items_api
function get_object(
    p_id in number)
return json_object_t
is
    c_get_object_approach constant t_get_object_approach := c_approach_sql;
    l_ret json_object_t;
begin
    case c_get_object_approach
        when c_approach_programmatic then
            l_ret := get_object_programmatically(p_id);
        when c_approach_sql then
            l_ret := json_object_t(get_object_using_sql(p_id));
    end case;
    return l_ret;
end;

GET_OBJECT_PROGRAMMATICALLY shown below selects the data from ACTION_ITEMS and ACTION_ITEMS_TEAM_MEMBERS tables into appropriate PL/SQL record structures. Then it uses the PL/SQL JSON_OBJECT_T and JSON_ARRAY_T types to create the action item JSON property by property. Notice that the code first builds up the contents of the JSON array, and then adds the array to the action item JSON object with a property name of team at the end.

-- in package action_items_api
function get_object_programmatically(
    p_id in number)
return json_object_t
is
    l_action_item_rec action_items%rowtype;
    l_action_item_obj json_object_t := json_object_t();
    l_team_members_arr json_array_t := json_array_t();
    l_team_members     t_action_item_team_members := t_action_item_team_members();
    l_team_member_obj  json_object_t;
    l_team_member_name staff.name%type;
begin
    app_common.report_if_error(
        action_items_common.ensure_action_exists(p_id));
    select *
      into l_action_item_rec
      from action_items
     where id = p_id;
    l_action_item_obj.put('_id',l_action_item_rec.id);
    l_action_item_obj.put('name',l_action_item_rec.name);
    l_action_item_obj.put('status',l_action_item_rec.status);
    select *
      bulk collect into l_team_members
      from action_item_team_members
     where action_id = p_id;
    for j in 1..l_team_members.count loop
        l_team_member_obj := json_object_t();
        l_team_member_obj.put('team_assignment_id',l_team_members(j).id);
        l_team_member_obj.put('role',l_team_members(j).role);
        l_team_member_obj.put('staff_id',l_team_members(j).user_id);
        -- Lookup staff name using id
        select name
          into l_team_member_name
          from staff
         where id = l_team_members(j).user_id;
        l_team_member_obj.put('name',l_team_member_name);
        -- Append team member object to the team array
        l_team_members_arr.append(l_team_member_obj);
    end loop;
    l_action_item_obj.put('team',l_team_members_arr);
    return l_action_item_obj;
end get_object_programmatically;

As shown below, GET_OBJECT_USING_SQL uses the JSON_OBJECT and JSON_ARRAYAGG functions to create the action item JSON object with its nested team members. Notice the RETURNING CLOB clauses on the array function and on the outer use of JSON_OBJECT to ensure the JSON construction is not limited to 4000 characters.

-- in package action_items_api
function get_object_using_sql(
    p_id in number)
return clob
is
    l_action_item_json_clob clob;
begin
    app_common.report_if_error(
        action_items_common.ensure_action_exists(p_id));
    select json_object(
      '_id' value ai.id,
      'name' value ai.name,
      'status' value ai.status,
      'team' value (
        select json_arrayagg(
          json_object(
            'team_assignment_id' value tm.id,
            'role' value tm.role,
            'staff_id' value tm.user_id,
            'name' value s.name
          )
        returning clob /* json_arrayagg */)
        from action_item_team_members tm
        left join staff s on tm.user_id = s.id
        where tm.action_id = ai.id
      )
    returning clob /* json_object */)
    into l_action_item_json_clob
    from action_items ai
    where ai.id = p_id;
    return l_action_item_json_clob;
end get_object_using_sql;

14.8.2.2 Retrieving Action Items with Paging#

Retrieve matching action items in a predictable order and return paged JSON results.

The GET_OBJECTS procedure accepts a search string, offset, and limit as parameters as shown below. Using a cursor FOR loop, it select the action item's primary key ID column. Interesting things to notice in the loop's query include:
  • COUNT(*) OVER() analytic function computes the total number of matching rows
  • WHERE clause filters action item name using search criteria (matching anything by default)
  • ORDER BY UPPER(NAME) guarantees predictable order for paging
  • OFFSET … ROWS skips indicated number of rows (skipping 0 by default)
  • FETCH FIRST … ROWS retrieves up to indicated row limit page size (25 by default).
Inside the loop, for each matching action item it calls GET_OBJECT to generate the JSON action item to add to an items array. This ensures consistent results across contexts and keeps the JSON construction logic in one place. The final JSON object GET_OBJECTS returns includes these properties:
  • items array containing the matching action item objects
  • hasMore boolean indicating whether there are more rows to return
  • totalRows number indicating how many action items the client could retrieve.
-- in package action_items_api
function get_objects(
    p_search in varchar2,
    p_offset in number default null,
    p_limit  in number default null)
return json_object_t
is
    l_offset pls_integer := nvl(p_offset,0);
    l_limit  pls_integer := nvl(p_limit,25);
    l_ret json_object_t := json_object_t();
    l_items json_array_t := json_array_t();
    l_row_count  pls_integer := 0;
    l_total_rows pls_integer := 0;
begin
    for j in (select id, count(*) over() as total_count
                from action_items
               where upper(name) like upper('%'||p_search||'%')
               order by upper(name)
               offset l_offset rows
               fetch first l_limit rows only)
    loop
        if l_total_rows = 0 then
            l_total_rows := j.total_count;
        end if;
        l_row_count := l_row_count + 1;
        l_items.append(get_object(j.id));
    end loop;
    l_ret.put('items',l_items);
    l_ret.put('hasMore', l_offset + l_row_count < l_total_rows);
    l_ret.put('totalRows',l_total_rows);
    return l_ret;
end get_objects;

14.8.2.3 Modifying Action Items with Pre/Post Logic#

Coordinate JSON parsing, business logic, and table changes for action item CRUD operations.

The INSERT_OBJECT, UPDATE_OBJECT, and DELETE_OBJECT functions coordinate the processing of the incoming JSON document to:
  • Populate PL/SQL records with the JSON action item and team member data
  • Apply before save and after save logic to compute defaults and validate what's needed
  • Save the valid data to ACTION_ITEMS and ACTION_ITEM_TEAM_MEMBERS tables.

14.8.2.4 Inspecting the Core Action Item API Body#

Inspect the full core API package body that coordinates JSON parsing, data changes, and validation.

The full code of the ACTION_ITEMS_API package body appears below.

create or replace package body action_items_api as

    subtype t_get_object_approach is pls_integer range 1..2;
    c_approach_programmatic constant t_get_object_approach := 1;
    c_approach_sql          constant t_get_object_approach := 2;

    subtype t_operation is pls_integer range 1..3;
    c_operation_insert      constant t_operation := 1;
    c_operation_update      constant t_operation := 2;
    c_operation_delete      constant t_operation := 3;

    ------------------------------------
    procedure before_save(
        p_action_item  in out action_items%rowtype,
        p_action_item_team_members in out t_action_item_team_members,
        p_operation    in     t_operation)
    is
    begin
        if p_operation = c_operation_insert then
            -- Providing an ID with insert not allowed
            app_common.report_if_error(
                action_items_common.ensure_null_id_for_new_action(p_action_item.id));
        end if;
        if p_operation in (c_operation_insert,c_operation_update) then
            -- Default role to member if not supplied
            for j in 1..p_action_item_team_members.count loop
                if p_action_item_team_members(j).role is null then
                    p_action_item_team_members(j).role := 'MEMBER';
                else
                    -- Check role value is legal
                    app_common.report_if_error(
                        action_items_common.is_valid_role(p_action_item_team_members(j).role));
                end if;
            end loop;
        end if;
    end before_save;
    ------------------------------------
    procedure save(
        p_action_item              in out action_items%rowtype,
        p_action_item_team_members in out t_action_item_team_members,
        p_operation                in     t_operation)
    is
        l_new_ids                apex_t_number;
        l_action_item_member_ids apex_t_number := apex_t_number();
    begin
        case p_operation
            when c_operation_insert then
                -- insert the action item
                insert into action_items(name)
                values (p_action_item.name)
                returning id into p_action_item.id;
                -- Set foreign key attribute for new child rows
                for i in 1..p_action_item_team_members.count loop
                    p_action_item_team_members(i).action_id := p_action_item.id;
                end loop;
                -- insert all the team members
                forall i in 1..p_action_item_team_members.count
                    insert into action_item_team_members values p_action_item_team_members(i)
                    returning id bulk collect into l_new_ids;
                -- Populate the system-assigned id
                for i in 1..p_action_item_team_members.count loop
                    p_action_item_team_members(i).id := l_new_ids(i);
                end loop;
            when c_operation_update then
                update action_items
                set name = p_action_item.name,
                    status = p_action_item.status
                where id = p_action_item.id;
                -- Insert/update child rows that need inserting, populating assigned id
                for i in 1..p_action_item_team_members.count loop
                    if p_action_item_team_members(i).id is null then
                        -- id is null? Needs an insert
                        insert into action_item_team_members
                        values p_action_item_team_members(i)
                        returning id into p_action_item_team_members(i).id;
                    else
                        -- id is not null, needs an update
                        update action_item_team_members
                        set role = p_action_item_team_members(i).role,
                            user_id = p_action_item_team_members(i).user_id
                        where id = p_action_item_team_members(i).id
                          and action_id = p_action_item_team_members(i).action_id;
                    end if;
                    -- track which ids we updated and inserted so we can delete others
                    l_action_item_member_ids.extend;
                    l_action_item_member_ids(l_action_item_member_ids.count)
                        := p_action_item_team_members(i).id;
                end loop;
                -- Delete any existing team members who are not among the updates
                delete from action_item_team_members
                where action_id = p_action_item.id
                and id not in (select column_value from table(l_action_item_member_ids));
            when c_operation_delete then
                delete from action_items
                 where id = p_action_item.id;
        end case;
    end save;
    ------------------------------------
    procedure after_save(
        p_action_item              in action_items%rowtype,
        p_action_item_team_members in t_action_item_team_members,
        p_operation                in t_operation)
    is
    begin
        if p_operation in (c_operation_insert,c_operation_update) then
            app_common.report_if_error(
                action_items_common.no_duplicate_team_members(p_action_item.id));
            app_common.report_if_error(
                action_items_common.one_lead_per_action_team(p_action_item.id));
        end if;
    end after_save;
    ------------------------------------
    procedure action_item_json_to_record(
        p_object        in json_object_t,
        p_action_item  out action_items%rowtype,
        p_action_item_team_members out t_action_item_team_members)
    is
        l_team_members_arr json_array_t;
        l_team_member_obj  json_object_t;
        l_team_member_rec  action_item_team_members%rowtype;
        l_team_member_name staff.name%type;
    begin
        p_action_item.id     := p_object.get_number('_id');
        p_action_item.name   := p_object.get_string('name');
        p_action_item.status := p_object.get_string('status');
        p_action_item_team_members := t_action_item_team_members();
        l_team_members_arr := p_object.get_array('team');
        for j in 0..l_team_members_arr.get_size - 1 loop
            -- Fill in foreign key attribute from parent
            l_team_member_rec := null;
            l_team_member_rec.action_id := p_action_item.id;
            l_team_member_obj := treat(l_team_members_arr.get(j) as json_object_t);
            l_team_member_rec.id      := l_team_member_obj.get_number('team_assignment_id');
            l_team_member_rec.user_id := l_team_member_obj.get_string('staff_id');
            -- if user_id is null, try to use name to look it up since name is unique
            if l_team_member_rec.user_id is null then
                l_team_member_name := l_team_member_obj.get_string('name');
                if l_team_member_name is not null then
                    begin
                        select id
                          into l_team_member_rec.user_id
                          from staff
                         where name = l_team_member_name;
                    exception
                        when no_data_found then
                            app_common.report_error_key(
                                'TEAM_MEMBER_NOT_FOUND',
                                apex_t_varchar2('name',l_team_member_name));
                    end;
                end if;
            end if;
            l_team_member_rec.role := substr(l_team_member_obj.get_string('role'),1,6);
            p_action_item_team_members.extend;
            p_action_item_team_members(p_action_item_team_members.count) := l_team_member_rec;
        end loop;
    end action_item_json_to_record;
    ------------------------------------
    function get_object_using_sql(
        p_id in number)
    return clob
    is
        l_action_item_json_clob clob;
    begin
        app_common.report_if_error(
            action_items_common.ensure_action_exists(p_id));
        select json_object(
          '_id' value ai.id,
          'name' value ai.name,
          'status' value ai.status,
          'team' value (
            select json_arrayagg(
              json_object(
                'team_assignment_id' value tm.id,
                'role' value tm.role,
                'staff_id' value tm.user_id,
                'name' value s.name
              )
            returning clob /* json_arrayagg */)
            from action_item_team_members tm
            left join staff s on tm.user_id = s.id
            where tm.action_id = ai.id
          )
        returning clob /* json_object */)
        into l_action_item_json_clob
        from action_items ai
        where ai.id = p_id;
        return l_action_item_json_clob;
    end get_object_using_sql;
    ------------------------------------
    function get_object_programmatically(
        p_id in number)
    return json_object_t
    is
        l_action_item_rec action_items%rowtype;
        l_action_item_obj json_object_t := json_object_t();
        l_team_members_arr json_array_t := json_array_t();
        l_team_members     t_action_item_team_members := t_action_item_team_members();
        l_team_member_obj  json_object_t;
        l_team_member_name staff.name%type;
    begin
        app_common.report_if_error(
            action_items_common.ensure_action_exists(p_id));
        select *
          into l_action_item_rec
          from action_items
         where id = p_id;
        l_action_item_obj.put('_id',l_action_item_rec.id);
        l_action_item_obj.put('name',l_action_item_rec.name);
        l_action_item_obj.put('status',l_action_item_rec.status);
        select *
          bulk collect into l_team_members
          from action_item_team_members
         where action_id = p_id;
        for j in 1..l_team_members.count loop
            l_team_member_obj := json_object_t();
            l_team_member_obj.put('team_assignment_id',l_team_members(j).id);
            l_team_member_obj.put('role',l_team_members(j).role);
            l_team_member_obj.put('staff_id',l_team_members(j).user_id);
            -- Lookup staff name using id
            select name
              into l_team_member_name
              from staff
             where id = l_team_members(j).user_id;
            l_team_member_obj.put('name',l_team_member_name);
            -- Append team member object to the team array
            l_team_members_arr.append(l_team_member_obj);
        end loop;
        l_action_item_obj.put('team',l_team_members_arr);
        return l_action_item_obj;
    end get_object_programmatically;
    ------------------------------------
    function get_object(
        p_id in number)
    return json_object_t
    is
        c_get_object_approach   constant t_get_object_approach := c_approach_sql;
        l_ret json_object_t;
    begin
        case c_get_object_approach
            when c_approach_programmatic then
                l_ret := get_object_programmatically(p_id);
            when c_approach_sql then
                l_ret := json_object_t(get_object_using_sql(p_id));
        end case;
        return l_ret;
    end;
    ------------------------------------
    function get_objects(
        p_search in varchar2,
        p_offset in number default null,
        p_limit  in number default null)
    return json_object_t
    is
        l_offset pls_integer := nvl(p_offset,0);
        l_limit  pls_integer := nvl(p_limit,25);
        l_ret json_object_t := json_object_t();
        l_items json_array_t := json_array_t();
        l_row_count  pls_integer := 0;
        l_total_rows pls_integer := 0;
    begin
        for j in (select id, count(*) over() as total_count
                    from action_items
                   where upper(name) like upper('%'||p_search||'%')
                   order by upper(name)
                   offset l_offset rows
                   fetch first l_limit rows only)
        loop
            if l_total_rows = 0 then
                l_total_rows := j.total_count;
            end if;
            l_row_count := l_row_count + 1;
            l_items.append(get_object(j.id));
        end loop;
        l_ret.put('items',l_items);
        l_ret.put('hasMore', l_offset + l_row_count < l_total_rows);
        l_ret.put('totalRows',l_total_rows);
        return l_ret;
    end get_objects;
    ------------------------------------
    function insert_object(
        p_object in json_object_t)
    return json_object_t
    is
        l_action_item_rec action_items%rowtype;
        l_action_item_team_members t_action_item_team_members;
    begin
        action_item_json_to_record(
            p_object                   => p_object,
            p_action_item              => l_action_item_rec,
            p_action_item_team_members => l_action_item_team_members);
        before_save(l_action_item_rec,l_action_item_team_members,c_operation_insert);
        save(l_action_item_rec,l_action_item_team_members,c_operation_insert);
        after_save(l_action_item_rec,l_action_item_team_members,c_operation_insert);
        return get_object(l_action_item_rec.id);
    end insert_object;
    ------------------------------------
    function update_object(
        p_object in json_object_t)
    return json_object_t
    is
        l_action_item_rec action_items%rowtype;
        l_action_item_team_members t_action_item_team_members;
    begin
        action_item_json_to_record(
            p_object                   => p_object,
            p_action_item              => l_action_item_rec,
            p_action_item_team_members => l_action_item_team_members);
        before_save(l_action_item_rec,l_action_item_team_members,c_operation_update);
        save(l_action_item_rec,l_action_item_team_members,c_operation_update);
        after_save(l_action_item_rec,l_action_item_team_members,c_operation_update);
        return get_object(l_action_item_rec.id);
    end update_object;
    ------------------------------------
    procedure delete_object(
        p_id in number)
    is
        l_action_item_rec action_items%rowtype;
        l_action_item_team_members t_action_item_team_members;
    begin
        l_action_item_rec.id := p_id;
        before_save(l_action_item_rec,l_action_item_team_members,c_operation_delete);
        save(l_action_item_rec,l_action_item_team_members,c_operation_delete);
        after_save(l_action_item_rec,l_action_item_team_members,c_operation_delete);
    end delete_object;
end action_items_api;

14.9.1.1 Storing and Accessing JSON Type Data#

Oracle 26ai JSON type to store, query, and transform JSON, with easy access from JSON_OBJECT_T when needed.

Oracle 26ai has a JSON data type for optimized storage and access of JSON data in SQL and PL/SQL. You can use JSON columns in relational tables, create JSON collection tables with their single DATA column of JSON type, and more easily and efficiently manipulate JSON in using SQL. For example, by working with the JSON data using a JSON type you can use the powerful JSON_TRANSFORM function to do bulk, conditional modification of JSON. Consider starting with a JSON document in a JSON variable like this:
l_actionitem json :=
             json(q'~
                {
                  "name": "Practice Using JSON in PL/SQL",
                  "team": [
                    {"name": "Georgia","role": "LEAD"},
                    {"name": "Quinn"},
                    {"name": "Zelda"}
                  ]
                }
              ~');
Using JSON_TRANSFORM you can default the role property to the value MEMBER on any team array object where role is null or is missing using the following declarative operation:
-- Default role to member if not supplied
select
   json_transform(l_actionitem,
      set '$.team[*]?(@.role == null || !exists(@.role)).role'
           = 'MEMBER')
    into l_actionitem;
You can use the full power of JSON path expressions to access any data in the document right from PL/SQL:
-- Print out the name of the LEAD team member
p(json_value(l_actionitem,'$.team[*]?(@.role == "LEAD").name'));
You can loop over array objects and process them in a cursor for loop:
-- Process each team member object in a loop
for k in (select team_member
            from json_table(l_actionitem,'$.team[*]'
                    columns (team_member json path '
You can selectively remove elements:
-- Delete team member Zelda using SQL
select json_transform(l_actionitem,
            remove '$.team[*]?(@.name == "Zelda" && @.role == "MEMBER")')
  into l_actionitem;
When needed, the JSON type interoperates seamlessly with the JSON_OBJECT_T API you may already be familiar with. Simply pass the JSON object to the JSON_OBJECT_T constructor function to work with it using that "interface" instead. Then later you can call TO_JSON on the JSON_OBJECT_T to get it back as a JSON type again. This example shows this back and forth in action:
-- Delete team member Quinn using PL/SQL JSON_OBJECT_T
l_actionitem_obj := json_object_t(l_actionitem);
l_teammembers    := l_actionitem_obj.get_array('team');
-- iterate backwards for deleting
for j in reverse 0..l_teammembers.get_size - 1  loop
    if get_object(l_teammembers,j).get_string('name') = 'Quinn' then
        l_teammembers.remove(j);
    end if;
end loop;
-- Show interoperability between JSON and JSON_OBJECT_T
l_actionitem := l_actionitem_obj.to_json;
The full code example follows below:
declare
    l_actionitem json :=
                 json(q'~
                    {
                      "name": "Practice Using JSON in PL/SQL",
                      "team": [
                        {"name": "Georgia","role": "LEAD"},
                        {"name": "Quinn"},
                        {"name": "Zelda"}
                      ]
                    }
                  ~');
    l_actionitem_obj json_object_t;
    l_teammembers    json_array_t;
    -----------------------------
    function get_object(
        p_array in json_array_t,
        p_index in pls_integer)
        return     json_object_t
    is
    begin
        return treat(p_array.get(p_index) as json_object_t);
    end get_object;
    -----------------------------
    procedure p(p_str in varchar)
    is
    begin
        dbms_output.put_line(p_str);
    end p;
begin
    -- Default role to member if not supplied
    select json_transform(l_actionitem,
                set '$.team[*]?(@.role == null
                                ||!exists(@.role))
                                .role' = 'MEMBER')
    into l_actionitem;
    p(json_serialize(l_actionitem pretty));
    -- Print out the name of the LEAD team member
    p(json_value(l_actionitem,'$.team[*]?(@.role == "LEAD").name'));
    -- Process each team member object in a loop
    for k in (select team_member
                from json_table(l_actionitem,'$.team[*]'
                        columns (team_member json path '
Running the PL/SQL block above produces the following output:
{
  "name" : "Practice Using JSON in PL/SQL",
  "team" :
  [
    {
      "name" : "Georgia",
      "role" : "LEAD"
    },
    {
      "name" : "Quinn",
      "role" : "MEMBER"
    },
    {
      "name" : "Zelda",
      "role" : "MEMBER"
    }
  ]
}
Georgia
Georgia,LEAD
Quinn,MEMBER
Zelda,MEMBER
{
  "name" : "Practice Using JSON in PL/SQL",
  "team" :
  [
    {
      "name" : "Georgia",
      "role" : "LEAD"
    },
    {
      "name" : "Quinn",
      "role" : "MEMBER"
    }
  ]
}
{
  "name" : "Practice Using JSON in PL/SQL",
  "team" :
  [
    {
      "name" : "Georgia",
      "role" : "LEAD"
    }
  ]
}
))) loop p(json_value(k.team_member,'$.name')||','|| json_value(k.team_member,'$.role')); end loop;