JSON GENERATE and PARSE

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against ISO/IEC 1989:2023 (edition 3, January 2023; an upcoming 4th edition is in ballot), which this page does not rely on for JSON GENERATE and JSON PARSE (they are IBM statements that other compilers have followed); against IBM Enterprise COBOL for z/OS 6.5 (PTFs through September 2026) on z/OS 3.2, where both statements are fully supported; and against GnuCOBOL 3.2 (28 July 2023, the latest release), which has a partial JSON GENERATE, no working JSON PARSE, and with which every program on it was compiled with cobc -x -Wall and run on 2026-10-20 (and again with -std=ibm, which printed the same results unless the text says otherwise). z/OS examples are labelled "z/OS; not run here" and are checked against the Language Reference and the Programming Guide.

JSON is the text format that web services, REST clients and most modern tools exchange, and a batch program that cannot write it is stuck behind a conversion step. COBOL has two statements for it: JSON GENERATE turns a data item, usually a group, into a JSON document, and JSON PARSE fills a data item from one. This page covers both statements phrase by phrase, the two special registers, the way COBOL data becomes JSON text and back, encodings, null and boolean values, the Circulation daily summary exported as JSON, and the portable way to build JSON with STRING when the compiler has no JSON GENERATE. The statement STRING itself is on Strings: STRING, UNSTRING and INSPECT; national and UTF-8 data are on National, UTF-8 and Code Pages; the XML counterparts are on XML GENERATE and PARSE; exception handling in general is on Error Handling and Declaratives.

Where the book is outdated: Taulli’s Modern Mainframe Development (2022) does not describe JSON GENERATE or JSON PARSE at all, so everything here comes from the Language Reference, the Programming Guide and the tests run for this page. Its z/OS 2.4 context is also old; the statements, their special registers and the exception codes below are those of Enterprise COBOL 6.5.

What the two statements do

JSON GENERATE reads the source item field by field, converts each value to JSON text, names it after the data name (or a name you give), and stores the whole document in a receiving item. JSON PARSE runs the other way: it reads the document, matches each JSON name with a data name, and moves each value into the matching field with the rules of a MOVE.

flowchart LR A["Circulation files: LOANTRAN, BOOKMAST, MEMBMAST"] --> B["Batch program: count and total"] B --> C["DAILY-SUMMARY group"] C --> D["JSON GENERATE or STRING"] D --> E["JSON text in a receiving item"] E --> F["SUMMARY.JSON file or message"] G["Inbound JSON request"] --> H["JSON PARSE"] H --> I["Request group"]

Group items become JSON objects, elementary items become strings, numbers or the literals true, false and null, and an OCCURS table becomes an array. The text is plain JSON as defined by RFC 8259 (see below), with no blanks between the tokens.

Who supports what

The table shows what each compiler accepts. The GnuCOBOL column is what the 3.2 compiler did with the programs on this page; the 3.2 manual should be consulted for anything not listed.

Feature Enterprise COBOL 6.5 GnuCOBOL 3.2 (tested)

JSON GENERATE ... FROM ...

Supported

Supported: group, elementary item, FILLER skipped

COUNT IN

Supported

Supported (counts bytes for an alphanumeric receiver)

COUNT BYTES IN

Supported from 6.5

Rejected: "'BYTES' is a reserved word, but isn’t supported"

NAME OF ... IS

Supported, including OMITTED

Supported with literals; OMITTED not tested

SUPPRESS item

Supported, also with WHEN and EVERY

Supported for a named item; SUPPRESS ... WHEN is a syntax error

CONVERTING (boolean, null)

Supported

Rejected: "syntax error, unexpected CONVERTING"

INDICATING

Supported

Rejected: syntax error

ENCODING

Supported

Rejected: syntax error

ON EXCEPTION, JSON-CODE

Supported

Supported; see the quirk with NOT ON EXCEPTION below

OCCURS tables in the source

Arrays

Warning: "OCCURS items in JSON/XML GENERATE is not implemented"

JSON PARSE

Supported, with WITH DETAIL, IGNORING, INDICATING, CONVERTING

Compiles with the warning "JSON PARSE is not implemented" and does nothing

JSON-STATUS

Supported (set by JSON PARSE)

Not meaningful without JSON PARSE

Programs that must run on both platforms therefore either confine themselves to the subset that 3.2 accepts, or build the document with STRING, as shown near the end of the page. Results from GnuCOBOL’s JSON GENERATE are also not guaranteed to match IBM’s in every detail; the fallback does not depend on either.

JSON GENERATE

The statement

The statement has a receiving item, a source item and a number of optional phrases, in this order:

JSON GENERATE receiver FROM source
    [COUNT BYTES IN byte-count]
    [COUNT [CHARACTERS] IN char-count]
    [INDICATING item IS JSON NULL USING value IN indicator ...]
    [ENCODING ccsid [FROM CODEPAGE]]
    [NAME OF item IS "json-name" | OMITTED ...]
    [SUPPRESS item | item WHEN ... | EVERY [NUMERIC|NONNUMERIC] WHEN ...]
    [CONVERTING item TO JSON BOOLEAN|NULL USING value ...]
    [ON EXCEPTION imperative-statement]
    [NOT ON EXCEPTION imperative-statement]
[END-JSON]

The outline above simplifies the Language Reference syntax diagram, whose repetition marks do not reproduce in text; ALSO separates repeated INDICATING and CONVERTING entries, and NAME OF takes one or more item IS "name" pairs.

  • Receiver. An alphanumeric, national or UTF-8 item, elementary or group (a group is treated as an elementary item of its category). It must not be JUSTIFIED, must not overlap the source, and must be big enough for the document: the Language Reference suggests two to three times the size of the source, because names and punctuation are added. On GnuCOBOL 3.2 the receiver may not be reference-modified ("JSON/XML GENERATE receiving item may not be reference modified"). On Enterprise COBOL 6.5 it may also be a dynamic-length (DYNAMIC) alphanumeric or UTF-8 item.

  • Source. A group or elementary item, which cannot be reference-modified or have a RENAMES clause. FILLER and unnamed items, REDEFINES items and their subordinates, and RENAMES items are ignored, so they do not appear in the output (the test below confirms it). Every other elementary item needs a usage other than pointers or object references, and the data names must be unique inside the source.

  • Scope terminator. END-JSON ends the statement; a period also does.

How data becomes JSON

Each elementary item is converted in four steps: to character form, trimmed, converted to the target encoding, then escaped. The rules, taken from the Language Reference, with what GnuCOBOL 3.2 printed for the same item:

Item Rule GnuCOBOL 3.2 result

Unsigned or signed DISPLAY numeric, PIC 9(8) value 20261020

A JSON number; leading zeros removed (down to one digit before the point), no + sign, - for a negative value

20261020

PIC S9(5)V99 value -12.50

An explicit period for the decimal point, trailing zeros after the point kept

-12.50

PIC 9(3)V99 value 0

0.00 after trimming (zeros before the point reduced to one)

0.00

COMP-3 and COMP numerics

Converted as if moved to an edited numeric item with the same digits

3.00 and 42

Alphanumeric PIC X(10) value CENTRAL

A string; trailing spaces removed (leading ones for a JUSTIFIED item)

"CENTRAL"

Alphanumeric item holding only spaces

One space kept

" "

Numeric-edited item PIC ZZ9.99

Not converted; it is text, so it becomes a string, leading space included

" 7.50"

Group item

A JSON object named after the group, with its subordinates inside

{"TOP-BOOK":{...}}

FILLER

Ignored

Not present

Quote and backslash

Escaped as \" and \\

\" and \\

Control characters (below U+0020)

Short escapes for backspace, tab, line feed, form feed and carriage return (\b \t \n \f \r) and \u00hh for the rest

A tab became \t

The names come from the data names with the spelling used in the data description, so Run-Date in the program gives "Run-Date" (GnuCOBOL 3.2 kept the mixed case in a test), and uppercase data names such as the Circulation ones give uppercase names with hyphens. Those are rarely the names a client wants, which is why NAME OF exists. The top-level object is named after the source item, so a document built from DAILY-SUMMARY is {"DAILY-SUMMARY":{...}} unless that item is renamed or its name is made OMITTED on Enterprise COBOL.

The Circulation daily summary

The nightly Circulation run ends with an export step. This program reads LOANTRAN, BOOKMAST and MEMBMAST (the copybooks and files of Copybooks and REPLACE), counts the day’s checkouts, returns and rejects, counts the catalog, totals the fines, and writes the summary as one JSON line. It uses COUNT to know how long the document is, NAME OF to give the fields JSON-style names, and SUPPRESS to keep an internal tracing field out of the document.

      * JSONSUM - Circulation daily summary exported as JSON
       IDENTIFICATION DIVISION.
       PROGRAM-ID. JSONSUM.
       ENVIRONMENT DIVISION.
       INPUT-OUTPUT SECTION.
       FILE-CONTROL.
           SELECT BOOK-FILE ASSIGN TO "BOOKMAST"
               ORGANIZATION IS LINE SEQUENTIAL.
           SELECT MEMBER-FILE ASSIGN TO "MEMBMAST"
               ORGANIZATION IS LINE SEQUENTIAL.
           SELECT LOAN-FILE ASSIGN TO "LOANTRAN"
               ORGANIZATION IS LINE SEQUENTIAL.
           SELECT JSON-FILE ASSIGN TO "SUMMARY.JSON"
               ORGANIZATION IS LINE SEQUENTIAL.
       DATA DIVISION.
       FILE SECTION.
       FD  BOOK-FILE.
           COPY BOOKREC.
       FD  MEMBER-FILE.
           COPY MEMBREC.
       FD  LOAN-FILE.
           COPY LOANREC.
       FD  JSON-FILE.
       01  JSON-LINE               PIC X(400).
       WORKING-STORAGE SECTION.
       01  EOF-FLAG                PIC X VALUE "N".
           88  AT-END              VALUE "Y".
       01  DAILY-SUMMARY.
           05  RUN-DATE            PIC 9(8) VALUE ZERO.
           05  LIBRARY-NAME        PIC X(30)
                                   VALUE 'CENTRAL "MAIN" LIBRARY'.
           05  LOAN-COUNTS.
               10  CHECKOUTS       PIC 9(4) VALUE ZERO.
               10  RETURNS         PIC 9(4) VALUE ZERO.
               10  REJECTS         PIC 9(4) VALUE ZERO.
           05  CATALOG.
               10  ACTIVE-TITLES   PIC 9(4) VALUE ZERO.
               10  WITHDRAWN-TITLES PIC 9(4) VALUE ZERO.
               10  COPIES-ON-SHELF PIC 9(5) VALUE ZERO.
           05  FINES-OUTSTANDING   PIC S9(7)V99 VALUE ZERO.
           05  TRACE-ID            PIC X(8) VALUE "T0042".
       01  JSON-TEXT               PIC X(400).
       01  JSON-LEN                PIC 9(4) VALUE ZERO.
       PROCEDURE DIVISION.
       MAIN-PARA.
           PERFORM COUNT-LOANS
           PERFORM COUNT-BOOKS
           PERFORM SUM-FINES
           JSON GENERATE JSON-TEXT FROM DAILY-SUMMARY
               COUNT IN JSON-LEN
               NAME OF DAILY-SUMMARY IS "dailySummary"
                       RUN-DATE IS "runDate"
                       LIBRARY-NAME IS "library"
                       LOAN-COUNTS IS "loans"
                       ACTIVE-TITLES IS "activeTitles"
                       WITHDRAWN-TITLES IS "withdrawnTitles"
                       COPIES-ON-SHELF IS "copiesOnShelf"
                       FINES-OUTSTANDING IS "finesOutstanding"
                       CHECKOUTS IS "checkouts"
                       RETURNS IS "returns"
                       REJECTS IS "rejects"
                       CATALOG IS "catalog"
               SUPPRESS TRACE-ID
               ON EXCEPTION
                   DISPLAY "JSON GENERATE failed, JSON-CODE "
                       JSON-CODE
                   STOP RUN
           END-JSON
           DISPLAY "Generated " JSON-LEN " characters:"
           DISPLAY JSON-TEXT(1:JSON-LEN)
           OPEN OUTPUT JSON-FILE
           MOVE JSON-TEXT(1:JSON-LEN) TO JSON-LINE
           WRITE JSON-LINE
           CLOSE JSON-FILE
           STOP RUN.
       COUNT-LOANS.
           OPEN INPUT LOAN-FILE
           PERFORM UNTIL AT-END
               READ LOAN-FILE
                   AT END SET AT-END TO TRUE
                   NOT AT END
                       IF RUN-DATE = ZERO
                           MOVE LOAN-DATE TO RUN-DATE
                       END-IF
                       EVALUATE TRUE
                           WHEN LOAN-CHECKOUT ADD 1 TO CHECKOUTS
                           WHEN LOAN-RETURN   ADD 1 TO RETURNS
                           WHEN OTHER         ADD 1 TO REJECTS
                       END-EVALUATE
               END-READ
           END-PERFORM
           CLOSE LOAN-FILE
           MOVE "N" TO EOF-FLAG.
       COUNT-BOOKS.
           OPEN INPUT BOOK-FILE
           PERFORM UNTIL AT-END
               READ BOOK-FILE
                   AT END SET AT-END TO TRUE
                   NOT AT END
                       IF BOOK-ACTIVE
                           ADD 1 TO ACTIVE-TITLES
                       ELSE
                           ADD 1 TO WITHDRAWN-TITLES
                       END-IF
                       ADD BOOK-COPIES TO COPIES-ON-SHELF
               END-READ
           END-PERFORM
           CLOSE BOOK-FILE
           MOVE "N" TO EOF-FLAG.
       SUM-FINES.
           OPEN INPUT MEMBER-FILE
           PERFORM UNTIL AT-END
               READ MEMBER-FILE
                   AT END SET AT-END TO TRUE
                   NOT AT END ADD MEMBER-FINES TO FINES-OUTSTANDING
               END-READ
           END-PERFORM
           CLOSE MEMBER-FILE.

Compiled with cobc -x -Wall -I. JSONSUM.cob (and with -std=ibm), no warnings, it prints:

Generated 0214 characters:
{"dailySummary":{"runDate":20261020,"library":"CENTRAL \"MAIN\" LIBRARY","loans":{"checkouts":3,"returns":1,"rejects":0},"catalog":{"activeTitles":3,"withdrawnTitles":1,"copiesOnShelf":7},"finesOutstanding":62.50}}

The file SUMMARY.JSON then holds the same line, and its pretty-printed form shows the structure:

{
  "dailySummary": {
    "runDate": 20261020,
    "library": "CENTRAL \"MAIN\" LIBRARY",
    "loans": { "checkouts": 3, "returns": 1, "rejects": 0 },
    "catalog": { "activeTitles": 3, "withdrawnTitles": 1, "copiesOnShelf": 7 },
    "finesOutstanding": 62.50
  }
}

Points worth noticing:

  • COUNT IN JSON-LEN gives the length of the document; JSON-TEXT(1:JSON-LEN) is the part to write, because the rest of the receiver is spaces. The count is in bytes for an alphanumeric receiver.

  • The group names LOAN-COUNTS and CATALOG became the nested objects loans and catalog, and TRACE-ID is missing because of SUPPRESS.

  • The quotes inside the library name were escaped, which makes the document valid; a hand-built string would have produced a broken one.

  • 62.50 is a JSON number. The value 62.50 has a trailing zero that JSON allows; leading zeros would not be allowed and the conversion removes them.

  • The WRITE to a LINE SEQUENTIAL file drops trailing spaces, so the file holds exactly the 214 characters and a line feed.

NAME, SUPPRESS and the other phrases

  • COUNT [CHARACTERS] IN. Receives the number of generated characters: double-bytes for a national receiver, Unicode code points for a UTF-8 receiver, bytes otherwise. It must be an integer item without a P in its PICTURE.

  • COUNT BYTES IN (Enterprise COBOL 6.5). Receives the number of bytes generated. It exists for UTF-8 receivers, where COUNT counts code points and the byte length is what a file record or a message needs. GnuCOBOL 3.2 rejects it, and the byte length there is the plain COUNT.

  • NAME OF item IS "name". Replaces the JSON name of the source item or one of its subordinates. If the same item is named twice, the last name wins. OMITTED generates an anonymous object or array, or a bare value, and is only valid for the source item itself: with a table B1 the statement JSON GENERATE out FROM b1 NAME OF b1 IS OMITTED produces a document that starts with [, which several JSON consumers expect.

  • SUPPRESS. Excludes items. SUPPRESS item removes that item (and, for a group, everything in it); with WHEN it removes an elementary item only when it equals ZERO, SPACE, LOW-VALUE or HIGH-VALUE; SUPPRESS EVERY [NUMERIC | NONNUMERIC] WHEN ... applies the test to all eligible items. A group is dropped only when all its items are dropped, but the outermost object is always written, even as {"name":{}}. The receiver must still be large enough for the document before suppression.

  • CONVERTING item TO JSON BOOLEAN USING condition-name. The item must be a one-byte PIC X item, and the condition name (a level 88 directly under it) or a one-byte literal selects true; any other value gives false. CONVERTING item TO JSON NULL USING SPACE (or ZERO, LOW-VALUE, HIGH-VALUE) generates null when the item equals the figurative constant.

  • INDICATING item IS JSON NULL USING value IN indicator. A separate one-byte indicator item says when the indicated item must be null; the indicator itself does not appear in the document.

  • ENCODING. See below.

  • ON EXCEPTION. See below.

JSON-CODE and JSON-STATUS

Two special registers report what happened. JSON-CODE is defined as PIC S9(9) USAGE BINARY VALUE 0; it is zero after a successful statement and holds an exception code otherwise. JSON-STATUS is set by JSON PARSE for conditions that do not stop the statement (see the parse section). Both are always set, whether or not an ON EXCEPTION phrase is given. Without ON EXCEPTION, control simply continues after the statement, so the program must test JSON-CODE itself or it will use an incomplete document.

The exception codes of JSON GENERATE are:

JSON-CODE Meaning

1

The receiver was too small. The COUNT IN item holds the number of positions actually generated.

2

The ENCODING CCSID was invalid or unsupported.

504

Not enough storage for the work area; increase the region size.

507

Internal error; the Language Environment level may need its latest PTFs.

500 to 599 (others)

Internal error; report it to the support organization.

When an exception occurs, generation stops and the contents of the receiver are undefined.

The next program makes both cases happen, a receiver of 20 bytes for a document that needs 43 and one of 200 bytes. It uses ON EXCEPTION alone and reads JSON-CODE afterwards.

      * JSONERR - receiver too small: JSON-CODE 1
       IDENTIFICATION DIVISION.
       PROGRAM-ID. JSONERR.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  LOAN-COUNTS.
           05  CHECKOUTS           PIC 9(4) VALUE 3.
           05  RETURNS             PIC 9(4) VALUE 1.
       01  SMALL-TEXT              PIC X(20).
       01  BIG-TEXT                PIC X(200).
       01  JSON-LEN                PIC 9(4) VALUE ZERO.
       PROCEDURE DIVISION.
       MAIN-PARA.
           JSON GENERATE SMALL-TEXT FROM LOAN-COUNTS
               COUNT IN JSON-LEN
               ON EXCEPTION
                   DISPLAY "small receiver: JSON-CODE " JSON-CODE
                       ", count " JSON-LEN
           END-JSON
           JSON GENERATE BIG-TEXT FROM LOAN-COUNTS
               COUNT IN JSON-LEN
               ON EXCEPTION
                   DISPLAY "big receiver: JSON-CODE " JSON-CODE
           END-JSON
           DISPLAY "big receiver finished, JSON-CODE " JSON-CODE
               ", " BIG-TEXT(1:JSON-LEN)
           STOP RUN.

Compiled with cobc -x -Wall JSONERR.cob (and with -std=ibm), no warnings, it prints:

small receiver: JSON-CODE +000000001, count 0043
big receiver finished, JSON-CODE +000000000, {"LOAN-COUNTS":{"CHECKOUTS":3,"RETURNS":1}}

JSON-CODE 1 is the value the manual gives. The count printed for the small receiver is 43, the length the document would have had, where the manual says IBM reports the positions actually generated; do not rely on the count after an exception on either platform, and check JSON-CODE instead.

A GnuCOBOL 3.2 quirk with NOT ON EXCEPTION

On Enterprise COBOL, ON EXCEPTION runs when an exception occurs and NOT ON EXCEPTION runs when none does. GnuCOBOL 3.2 behaved differently when the statement has both phrases: this program prints what each case ran.

      * JSONQRK - ON EXCEPTION together with NOT ON EXCEPTION
       IDENTIFICATION DIVISION.
       PROGRAM-ID. JSONQRK.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  LOAN-COUNTS.
           05  CHECKOUTS           PIC 9(4) VALUE 3.
       01  TEXT-OK                 PIC X(200).
       01  TEXT-SMALL              PIC X(10).
       PROCEDURE DIVISION.
       MAIN-PARA.
           JSON GENERATE TEXT-OK FROM LOAN-COUNTS
               ON EXCEPTION DISPLAY "ok: ON EXCEPTION ran"
               NOT ON EXCEPTION DISPLAY "ok: NOT ON EXCEPTION ran"
           END-JSON
           DISPLAY "ok done, JSON-CODE " JSON-CODE
           JSON GENERATE TEXT-SMALL FROM LOAN-COUNTS
               ON EXCEPTION DISPLAY "small: ON EXCEPTION ran"
               NOT ON EXCEPTION DISPLAY "small: NOT ON EXC. ran"
           END-JSON
           DISPLAY "small done, JSON-CODE " JSON-CODE
           JSON GENERATE TEXT-SMALL FROM LOAN-COUNTS
               NOT ON EXCEPTION DISPLAY "only-NOT: ran"
           END-JSON
           DISPLAY "only-NOT done, JSON-CODE " JSON-CODE
           JSON GENERATE TEXT-OK FROM LOAN-COUNTS
               NOT ON EXCEPTION DISPLAY "only-NOT ok: ran"
           END-JSON
           DISPLAY "only-NOT ok done, JSON-CODE " JSON-CODE
           STOP RUN.

Compiled with cobc -x -Wall JSONQRK.cob (and with -std=ibm), no warnings, it prints:

ok done, JSON-CODE +000000000
small: ON EXCEPTION ran
small: NOT ON EXC. ran
small done, JSON-CODE +000000001
only-NOT done, JSON-CODE +000000001
only-NOT ok: ran
only-NOT ok done, JSON-CODE +000000000

With both phrases, a successful statement ran neither branch, and a failing one ran both. NOT ON EXCEPTION alone worked: it ran on success and did not run on failure. The portable pattern is therefore to omit NOT ON EXCEPTION and to test JSON-CODE after END-JSON, as JSONERR does; it behaves the same on both compilers.

Encoding, UTF-8 and RFC 8259

RFC 8259 is the specification of JSON text that the statements follow. The points that matter to a COBOL programmer:

  • Text exchanged between systems must be encoded in UTF-8 (section 8.1 of the RFC; the UTF-8 encoding itself is RFC 3629). A byte order mark must not be added.

  • A string must escape the quotation mark, the reverse solidus and all control characters from U+0000 to U+001F; other characters may be written as they are.

  • A number has no leading zeros (other than the single zero before a point), no + sign, and no hexadecimal, NaN or infinity forms, and the literals true, false and null are lower case. This is why the conversion trims numbers and why a PIC X item holding 007 is a string, not a number.

  • Names inside an object should be unique, and a top-level value can be any JSON value, not only an object.

On Enterprise COBOL the encoding of the generated text depends on the receiver and on ENCODING:

Receiver Default text ENCODING values

Alphanumeric PIC X

UTF-8 (CCSID 1208)

1208, or a single-byte EBCDIC CCSID such as 1047, 1140 or 37 (with FROM CODEPAGE, the CODEPAGE compiler option’s CCSID)

National PIC N / USAGE NATIONAL

UTF-16 (CCSID 1200)

1200 only

USAGE UTF-8 / PIC U

UTF-8

1208 only

The consequence on z/OS is that a PIC X receiver holds UTF-8 bytes, not EBCDIC characters. They are right for a file or a message sent to another system, and unreadable if the program DISPLAY`s them to the job log, which does not translate them. A program that wants a readable log line uses `ENCODING 1047 (or the CCSID of its CODEPAGE), or converts with FUNCTION DISPLAY-OF. With a UTF-8 or national receiver, COUNT counts code points or double-bytes while COUNT BYTES counts bytes: the title "Les Misérables" is 14 code points and 15 bytes in UTF-8. The same distinction is made on National, UTF-8 and Code Pages.

GnuCOBOL 3.2 has no ENCODING phrase and does no conversion: the bytes of the data item are copied into the document. On a UTF-8 system, a PIC X item that holds UTF-8 text (a source file in UTF-8 and a literal "Les Misérables") gave {"BOOK-JSON":{"BOOK-TITLE":"Les Misérables"}} with a COUNT of 46, which counts bytes, not characters. The same program with a file or data in a single-byte code page such as ISO 8859-1 would produce a document that is not valid UTF-8, so such data is converted before it is generated.

Null values and booleans

COBOL data has no null, so both statements need a rule for turning a value into null and back.

Direction Mechanism Effect

Generate

CONVERTING item TO JSON NULL USING SPACE

null when the item equals the figurative constant; otherwise the normal value

Generate

INDICATING item IS JSON NULL USING "Y" IN flag

null when flag holds Y; the flag is not written

Generate

CONVERTING item TO JSON BOOLEAN USING condition-name

true when the item matches the condition name, false otherwise

Parse

IGNORING JSON NULL FOR item (or ALL)

A null leaves the item unchanged and is not reported in JSON-STATUS

Parse

CONVERTING item FROM JSON NULL USING SPACE

The figurative constant is moved to the item when the value is null

Parse

INDICATING item IS JSON NULL USING "Y" AND "N" IN flag

Y is moved to the flag for a null, N otherwise; the item is not changed

Parse

CONVERTING item FROM JSON BOOLEAN USING "Y" AND "N"

true moves Y and false moves N

Parse

none of the above

A null leaves the item unchanged and sets JSON-STATUS bit 32

The Language Reference points out that not every item can be used with these phrases: the figurative constant must be a legal MOVE source for the item. GnuCOBOL 3.2 rejects all the phrases in the table.

z/OS: booleans, nulls, encoding and COUNT BYTES

The following fragment exports one book as a JSON object for a catalog web service. It converts the status to a boolean, makes an unknown price null, produces UTF-8 in a dynamic-length receiver, and reads both counts. z/OS; not run here. It is checked against the syntax and rules of the Language Reference for 6.5.

       01  BOOK-OUT.
           05  BOOK-ID             PIC 9(6).
           05  BOOK-TITLE          PIC X(30).
           05  BOOK-STATUS         PIC X.
               88  BOOK-ACTIVE     VALUE "A".
           05  BOOK-PRICE          PIC 9(4)V99.
           05  PRICE-FLAG          PIC X.
       01  BOOK-JSON               PIC U DYNAMIC LIMIT 400.
       01  JSON-CHARS              PIC 9(4) COMP.
       01  JSON-BYTES              PIC 9(4) COMP.
      *
           MOVE 1 TO BOOK-ID
           MOVE "DATA STRUCTURES" TO BOOK-TITLE
           MOVE "A" TO BOOK-STATUS
           MOVE "Y" TO PRICE-FLAG
           JSON GENERATE BOOK-JSON FROM BOOK-OUT
               COUNT BYTES IN JSON-BYTES
               COUNT IN JSON-CHARS
               INDICATING BOOK-PRICE IS JSON NULL USING "Y"
                   IN PRICE-FLAG
               NAME OF BOOK-OUT IS "book"
               CONVERTING BOOK-STATUS TO JSON BOOLEAN
                   USING BOOK-ACTIVE
               ON EXCEPTION
                   DISPLAY "JSON GENERATE failed, JSON-CODE " JSON-CODE
           END-JSON

The rules give this document, with JSON-CHARS equal to JSON-BYTES because the text is pure ASCII:

{"book":{"BOOK-ID":1,"BOOK-TITLE":"DATA STRUCTURES","BOOK-STATUS":true,"BOOK-PRICE":null}}

Notes: PRICE-FLAG is not written because an indicator is not part of the document; a PIC U DYNAMIC receiver (UTF-8) is produced as UTF-8 and cannot be given an ENCODING other than 1208; and the clauses are written in the order the syntax diagram shows them.

JSON PARSE

The statement

JSON PARSE json-text INTO receiver
    [WITH DETAIL]
    [IGNORING JSON NULL FOR ALL | item ...]
    [INDICATING item IS JSON NULL USING true-value AND false-value
        [IN indicator] ...]
    [ENCODING ccsid [FROM CODEPAGE]]
    [NAME OF item IS "json-name" | OMITTED ...]
    [SUPPRESS item ...]
    [CONVERTING item FROM JSON BOOLEAN|NULL USING ...]
    [ON EXCEPTION imperative-statement]
    [NOT ON EXCEPTION imperative-statement]
[END-JSON]
  • The text. An alphanumeric, national or UTF-8 item, elementary or group, assumed to be UTF-8 unless ENCODING says otherwise. It must not overlap the receiver. Unicode control characters, the quotation mark and the reverse solidus must be escaped in it (the escapes of the JSON specification are accepted, and so is \x, which JSON GENERATE writes for the EBCDIC new-line character).

  • The receiver. A group or elementary item; the same restrictions as the source of JSON GENERATE (no FILLER, no REDEFINES targets, unique names). The OCCURS DEPENDING ON object of a table must be outside the receiver and is not updated by the statement.

  • Matching. The nesting of the JSON objects must follow the nesting of the groups, names are matched with the data names case-sensitively (a JSON name such as bookId can never match a hyphenated data name such as BOOK-ID, which is why NAME OF is nearly always needed, and a NAME literal is compared case-sensitively), and the value is moved with MOVE semantics, so 5 into PIC 9(3) gives 005 and -9.0 into PIC S99V9 gives a signed value. Arrays fill OCCURS tables from the first element.

  • SUPPRESS. Excludes an item and its subordinates from the assignment; it overrides NAME.

  • WITH DETAIL. Issues runtime messages for the conditions that are not exceptions and for the exceptions (messages in the IGZ03 range, listed in Appendix D of the Programming Guide), which is the way to see which name failed to match. Without it, only the special registers tell.

  • Two names to take care of. A parse into a group needs the name of the top-level object to match the receiver’s name (or a NAME OF receiver IS "..."), and the CODEPAGE compiler option must name a single-byte EBCDIC CCSID.

JSON-CODE and JSON-STATUS after a parse

After JSON PARSE, JSON-CODE is zero or the exception code that stopped the statement, with the receiver partly modified. JSON-STATUS holds the sum of the codes for conditions that did not stop the statement, so test it by bit, not by equality. The Programming Guide shows a MOD and divide test; the comparison FUNCTION MOD(JSON-STATUS, 2 * code) >= code does the same without a fractional intermediate result.

JSON-STATUS bit Condition (not an exception)

1

A data item had no matching JSON pair and was not changed

2

A JSON pair matched no data item

4

A data item had several matching pairs with the same value

8

A table had more elements than the JSON array

16

A JSON array had more values than the table

32

An item was not changed because its value was null

64

Table elements were not changed because of null values

128

A size error in a numeric assignment (the item was modified anyway)

256

Loss of information in an alphanumeric assignment (modified anyway)

512

A substitution character appeared in the conversion from Unicode to the CODEPAGE CCSID

JSON-CODE Exception (the statement stops)

100

The JSON text was invalid

101

The text was empty or only blanks

102

Extra non-blank characters followed the closing brace of the outermost object

103

An item had several matching pairs with different values; the leftmost was kept

104

A value was incompatible with the matching data item

105

A matching pair had the value true or false

106

No JSON pair matched any data item

107

A true or false value matched an item that is not in a CONVERTING BOOLEAN phrase

108

An anonymous array was parsed without NAME ... IS OMITTED and an OCCURS clause

109

The ENCODING CCSID was invalid or unsupported

The Circulation inquiry service receives a request object and the program parses it. z/OS; not run here. It is checked against the Language Reference syntax for 6.5.

       01  REQUEST-TEXT            PIC X(200).
       01  INQUIRY-REQUEST.
           05  BOOK-ID             PIC 9(6).
           05  MEMBER-ID           PIC 9(6).
           05  WANT-HOLD           PIC X.
               88  HOLD-REQUESTED  VALUE "Y".
           05  NOTE-TEXT           PIC X(30).
           05  AUDIT-TAG           PIC X(8).
      *
      *    REQUEST-TEXT holds, in UTF-8:
      *    {"inquiry":{"bookId":1,"memberId":100,"hold":true,
      *     "note":null,"extra":5}}
           JSON PARSE REQUEST-TEXT INTO INQUIRY-REQUEST
               WITH DETAIL
               IGNORING JSON NULL FOR NOTE-TEXT
               NAME OF INQUIRY-REQUEST IS "inquiry"
                       BOOK-ID IS "bookId"
                       MEMBER-ID IS "memberId"
                       WANT-HOLD IS "hold"
                       NOTE-TEXT IS "note"
               SUPPRESS AUDIT-TAG
               CONVERTING WANT-HOLD FROM JSON BOOLEAN
                   USING "Y" AND "N"
               ON EXCEPTION
                   DISPLAY "request rejected, JSON-CODE " JSON-CODE
               NOT ON EXCEPTION
                   IF FUNCTION MOD(JSON-STATUS, 4) >= 2
                       DISPLAY "request had a field we do not know"
                   END-IF
           END-JSON

By the rules: BOOK-ID receives 000001, MEMBER-ID 000100, WANT-HOLD Y (from true), NOTE-TEXT is left as it was (the ignored null), AUDIT-TAG is not assigned, and the unknown extra pair sets JSON-STATUS bit 2 without an exception, with an informational runtime message because of WITH DETAIL. Had hold been missing, bit 1 would be set instead. A request whose text is {"other":1} would raise JSON-CODE 106, and a bookId of "abc" 104, so the ON EXCEPTION branch returns an error to the caller.

JSON PARSE on GnuCOBOL 3.2

GnuCOBOL 3.2 recognises the statement and does nothing with it. This program parses the same kind of request:

      * JSONPRS - JSON PARSE on GnuCOBOL 3.2
       IDENTIFICATION DIVISION.
       PROGRAM-ID. JSONPRS.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  INQUIRY-TEXT            PIC X(80)
           VALUE '{"inquiry":{"bookId":1,"copies":2}}'.
       01  INQUIRY.
           05  BOOK-ID             PIC 9(6) VALUE 999999.
           05  COPIES              PIC 9(3) VALUE ZERO.
       PROCEDURE DIVISION.
       MAIN-PARA.
           JSON PARSE INQUIRY-TEXT INTO INQUIRY
               ON EXCEPTION
                   DISPLAY "parse failed, JSON-CODE " JSON-CODE
           END-JSON
           DISPLAY "BOOK-ID " BOOK-ID " COPIES " COPIES
           STOP RUN.

Compiled with cobc -x -Wall JSONPRS.cob, it gives two warnings (the statement is flagged at its first line and at END-JSON), and the program still runs and leaves the receiver unchanged:

JSONPRS.cob: in paragraph 'MAIN-PARA':
JSONPRS.cob:13: warning: JSON PARSE is not implemented [-Wpending]
   11 |        PROCEDURE DIVISION.
   12 |        MAIN-PARA.
   13 >            JSON PARSE INQUIRY-TEXT INTO INQUIRY
   14 |                ON EXCEPTION
   15 |                    DISPLAY "parse failed, JSON-CODE " JSON-CODE
JSONPRS.cob:16: warning: JSON PARSE is not implemented [-Wpending]
   14 |                ON EXCEPTION
   15 |                    DISPLAY "parse failed, JSON-CODE " JSON-CODE
   16 >            END-JSON
   17 |            DISPLAY "BOOK-ID " BOOK-ID " COPIES " COPIES
   18 |            STOP RUN.
--- compiled (warnings above, if any) ---
BOOK-ID 999999 COPIES 000

Neither branch of ON EXCEPTION ran, JSON-CODE stayed zero, and the receiver kept its initial values, so a program that depends on JSON PARSE must not be run on 3.2 on the assumption that a warning-free run proves it worked. Inbound JSON on that platform is read with UNSTRING or INSPECT for simple fixed shapes (see Strings: STRING, UNSTRING and INSPECT), or by calling a C parser (see Interoperability with C, Java and Python).

The portable fallback: building JSON with STRING

When the compiler lacks JSON GENERATE (some do) or its phrases (CONVERTING, ENCODING, COUNT BYTES are not in 3.2), the document can be built with STRING. Three jobs have to be done by hand: edit numbers into JSON form (no leading zeros, a period, a minus sign), escape the text, and track the length with WITH POINTER. This program builds the same summary as JSONSUM, with a paragraph that escapes quotes, backslashes and control characters, and the numeric-edited items trimmed with FUNCTION TRIM.

      * JSONSTR - the same summary built with STRING (portable)
       IDENTIFICATION DIVISION.
       PROGRAM-ID. JSONSTR.
       ENVIRONMENT DIVISION.
       INPUT-OUTPUT SECTION.
       FILE-CONTROL.
           SELECT BOOK-FILE ASSIGN TO "BOOKMAST"
               ORGANIZATION IS LINE SEQUENTIAL.
           SELECT MEMBER-FILE ASSIGN TO "MEMBMAST"
               ORGANIZATION IS LINE SEQUENTIAL.
           SELECT LOAN-FILE ASSIGN TO "LOANTRAN"
               ORGANIZATION IS LINE SEQUENTIAL.
           SELECT JSON-FILE ASSIGN TO "SUMMARY2.JSON"
               ORGANIZATION IS LINE SEQUENTIAL.
       DATA DIVISION.
       FILE SECTION.
       FD  BOOK-FILE.
           COPY BOOKREC.
       FD  MEMBER-FILE.
           COPY MEMBREC.
       FD  LOAN-FILE.
           COPY LOANREC.
       FD  JSON-FILE.
       01  JSON-LINE               PIC X(400).
       WORKING-STORAGE SECTION.
       01  EOF-FLAG                PIC X VALUE "N".
           88  AT-END              VALUE "Y".
       01  RUN-DATE                PIC 9(8) VALUE ZERO.
       01  LIBRARY-NAME            PIC X(30)
                                   VALUE 'CENTRAL "MAIN" LIBRARY'.
       01  CHECKOUTS               PIC 9(4) VALUE ZERO.
       01  RETURNS                 PIC 9(4) VALUE ZERO.
       01  REJECTS                 PIC 9(4) VALUE ZERO.
       01  ACTIVE-TITLES           PIC 9(4) VALUE ZERO.
       01  WITHDRAWN-TITLES        PIC 9(4) VALUE ZERO.
       01  COPIES-ON-SHELF         PIC 9(5) VALUE ZERO.
       01  FINES-OUTSTANDING       PIC S9(7)V99 VALUE ZERO.
      * numeric edited items: trimmed text for the JSON number
       01  N-DATE                  PIC Z(7)9.
       01  N-COUNT                 PIC Z(4)9.
       01  N-COUNT2                PIC Z(4)9.
       01  N-COUNT3                PIC Z(4)9.
       01  N-ACTIVE                PIC Z(4)9.
       01  N-WITHDRAWN             PIC Z(4)9.
       01  N-COPIES                PIC Z(4)9.
       01  N-FINES                 PIC -(7)9.99.
      * escaping of a text value
       01  RAW-TEXT                PIC X(30).
       01  ESC-TEXT                PIC X(90).
       01  ESC-LEN                 PIC 99 COMP VALUE ZERO.
       01  RAW-LEN                 PIC 99 COMP VALUE ZERO.
       01  IDX                     PIC 99 COMP VALUE ZERO.
       01  ONE-CHAR                      PIC X.
       01  CODE-VAL                 PIC 999 COMP VALUE ZERO.
       01  HI-NIBBLE               PIC 99 COMP VALUE ZERO.
       01  LO-NIBBLE               PIC 99 COMP VALUE ZERO.
       01  HEX-DIGITS              PIC X(16) VALUE "0123456789abcdef".
       01  JSON-TEXT               PIC X(400).
       01  PTR                     PIC 9(4) COMP VALUE 1.
       PROCEDURE DIVISION.
       MAIN-PARA.
           PERFORM COUNT-LOANS
           PERFORM COUNT-BOOKS
           PERFORM SUM-FINES
           MOVE LIBRARY-NAME TO RAW-TEXT
           PERFORM ESCAPE-TEXT
           MOVE RUN-DATE TO N-DATE
           MOVE CHECKOUTS TO N-COUNT
           MOVE RETURNS TO N-COUNT2
           MOVE REJECTS TO N-COUNT3
           MOVE ACTIVE-TITLES TO N-ACTIVE
           MOVE WITHDRAWN-TITLES TO N-WITHDRAWN
           MOVE COPIES-ON-SHELF TO N-COPIES
           MOVE FINES-OUTSTANDING TO N-FINES
           MOVE SPACES TO JSON-TEXT
           MOVE 1 TO PTR
           STRING '{"dailySummary":{"runDate":'
                  FUNCTION TRIM(N-DATE)
                  ',"library":"' ESC-TEXT(1:ESC-LEN)
                  '","loans":{"checkouts":' FUNCTION TRIM(N-COUNT)
                  ',"returns":' FUNCTION TRIM(N-COUNT2)
                  ',"rejects":' FUNCTION TRIM(N-COUNT3)
                  '},"catalog":{"activeTitles":' FUNCTION TRIM(N-ACTIVE)
                  ',"withdrawnTitles":' FUNCTION TRIM(N-WITHDRAWN)
                  ',"copiesOnShelf":' FUNCTION TRIM(N-COPIES)
                  '},"finesOutstanding":' FUNCTION TRIM(N-FINES)
                  '}}'
               DELIMITED BY SIZE
               INTO JSON-TEXT WITH POINTER PTR
               ON OVERFLOW
                   DISPLAY "JSON text does not fit"
                   STOP RUN
           END-STRING
           SUBTRACT 1 FROM PTR
           DISPLAY "Built " PTR " characters:"
           DISPLAY JSON-TEXT(1:PTR)
           OPEN OUTPUT JSON-FILE
           MOVE JSON-TEXT(1:PTR) TO JSON-LINE
           WRITE JSON-LINE
           CLOSE JSON-FILE
           STOP RUN.
       ESCAPE-TEXT.
           MOVE SPACES TO ESC-TEXT
           MOVE ZERO TO ESC-LEN
           COMPUTE RAW-LEN = FUNCTION LENGTH(FUNCTION TRIM(RAW-TEXT
               TRAILING))
           PERFORM VARYING IDX FROM 1 BY 1 UNTIL IDX > RAW-LEN
               MOVE RAW-TEXT(IDX:1) TO ONE-CHAR
               COMPUTE CODE-VAL = FUNCTION ORD(ONE-CHAR) - 1
               EVALUATE TRUE
                   WHEN ONE-CHAR = QUOTE OR ONE-CHAR = "\"
                       ADD 1 TO ESC-LEN
                       MOVE "\" TO ESC-TEXT(ESC-LEN:1)
                       ADD 1 TO ESC-LEN
                       MOVE ONE-CHAR TO ESC-TEXT(ESC-LEN:1)
                   WHEN CODE-VAL < 32
                       MOVE "\u00" TO ESC-TEXT(ESC-LEN + 1:4)
                       ADD 4 TO ESC-LEN
                       ADD 1 TO ESC-LEN
                       MOVE HEX-DIGITS(CODE-VAL / 16 + 1:1)
                           TO ESC-TEXT(ESC-LEN:1)
                       ADD 1 TO ESC-LEN
                       MOVE HEX-DIGITS(FUNCTION MOD(CODE-VAL, 16) + 1:1)
                           TO ESC-TEXT(ESC-LEN:1)
                   WHEN OTHER
                       ADD 1 TO ESC-LEN
                       MOVE ONE-CHAR TO ESC-TEXT(ESC-LEN:1)
               END-EVALUATE
           END-PERFORM.
       COUNT-LOANS.
           OPEN INPUT LOAN-FILE
           PERFORM UNTIL AT-END
               READ LOAN-FILE
                   AT END SET AT-END TO TRUE
                   NOT AT END
                       IF RUN-DATE = ZERO
                           MOVE LOAN-DATE TO RUN-DATE
                       END-IF
                       EVALUATE TRUE
                           WHEN LOAN-CHECKOUT ADD 1 TO CHECKOUTS
                           WHEN LOAN-RETURN   ADD 1 TO RETURNS
                           WHEN OTHER         ADD 1 TO REJECTS
                       END-EVALUATE
               END-READ
           END-PERFORM
           CLOSE LOAN-FILE
           MOVE "N" TO EOF-FLAG.
       COUNT-BOOKS.
           OPEN INPUT BOOK-FILE
           PERFORM UNTIL AT-END
               READ BOOK-FILE
                   AT END SET AT-END TO TRUE
                   NOT AT END
                       IF BOOK-ACTIVE
                           ADD 1 TO ACTIVE-TITLES
                       ELSE
                           ADD 1 TO WITHDRAWN-TITLES
                       END-IF
                       ADD BOOK-COPIES TO COPIES-ON-SHELF
               END-READ
           END-PERFORM
           CLOSE BOOK-FILE
           MOVE "N" TO EOF-FLAG.
       SUM-FINES.
           OPEN INPUT MEMBER-FILE
           PERFORM UNTIL AT-END
               READ MEMBER-FILE
                   AT END SET AT-END TO TRUE
                   NOT AT END ADD MEMBER-FINES TO FINES-OUTSTANDING
               END-READ
           END-PERFORM
           CLOSE MEMBER-FILE.

Compiled with cobc -x -Wall -I. JSONSTR.cob, no warnings, it prints:

Built 0214 characters:
{"dailySummary":{"runDate":20261020,"library":"CENTRAL \"MAIN\" LIBRARY","loans":{"checkouts":3,"returns":1,"rejects":0},"catalog":{"activeTitles":3,"withdrawnTitles":1,"copiesOnShelf":7},"finesOutstanding":62.50}}

The file it wrote is byte-for-byte identical to the one JSONSUM wrote: cmp SUMMARY.JSON SUMMARY2.JSON printed nothing. With -std=ibm the output is the same except that the pointer prints with five digits (00214), because the binary item is then not truncated to its four-digit picture.

Limits of the fallback: it knows only the fields it is written for, so a new field means new code; it escapes control characters as \u00hh where IBM writes \t, which is equivalent JSON; and it does not convert code pages (the bytes are those of the program’s data, so under an EBCDIC compiler they would be EBCDIC and the consumer would have to expect that). For output that goes to non-COBOL consumers on z/OS, JSON GENERATE with its ENCODING is the better tool.

Common mistakes

  • Printing the whole receiver. The receiver is longer than the document and is padded with spaces. Use COUNT and reference modification, receiver(1:count).

  • A receiver that is too small. The statement fails with JSON-CODE 1 and the receiver is undefined. Size it at two to three times the source and always test JSON-CODE.

  • Not testing JSON-CODE without ON EXCEPTION. Control continues and the program uses an incomplete document.

  • Using NOT ON EXCEPTION together with ON EXCEPTION on GnuCOBOL 3.2. Neither branch ran on success. Test JSON-CODE after END-JSON.

  • Trusting the names. Data names such as BOOK-ID are not the names an API client expects, and a parse of bookId does not match them. Use NAME OF.

  • Numeric-edited fields in the source. They become strings with their blanks and punctuation (" 7.50"), not numbers.

  • Assuming EBCDIC output. On z/OS the default output of a PIC X receiver is UTF-8; DISPLAY does not translate it.

  • Forgetting COUNT BYTES. For a UTF-8 receiver COUNT counts code points, so a record length computed from it is too short for non-ASCII text.

  • Checking JSON-STATUS by equality. The codes are added together; test the bit.

  • Running JSON PARSE on GnuCOBOL 3.2. It compiles with a warning and does nothing.

  • Unescaped text in a STRING-built document. A single quote or backslash in a title makes the document invalid.