COBOL Textbook

COBOL JSON GENERATE and JSON PARSE: what GnuCOBOL 3.2 can do

A GnuCOBOL program that turns fixed-width records into JSON Lines with JSON GENERATE, the exact output it should write, and why JSON PARSE does nothing in 3.2.

Published
Dialect
GnuCOBOL 3.x, free format
Status
Desk-checked, not compiled
Length
3,271 words, 15 min

Sooner or later a COBOL file has to reach something that only reads JSON: a web service, a message queue, a data team's notebook. This lesson builds the program that does it in GnuCOBOL. It reads a fixed-width account file and writes JSON Lines, one JSON object per line, with the JSON GENERATE statement. Then it takes up the question that comes straight after, how to get JSON back into a record, where the answer for GnuCOBOL 3.2 is less comfortable than the compiler makes it look.

What was run and what was not. cobc is not installed on the machine this lesson was prepared on, so the COBOL listing has not been compiled. We desk-checked it against the GnuCOBOL Programmer's Guide, which documents the July 2020 build of 3.1 RC-1 and says almost nothing about JSON, and against the source code and test suite of the GnuCOBOL 3.2 release itself, downloaded from the GNU project's server on October 5, 2026. The output shown below comes from the Python companion, /code/cobol-json-generate-parse.py. It copies 3.2's formatting rules, checks its copy against the expected results in GnuCOBOL's own JSON tests and against IBM's documented examples, and only then applies the rules to the data. It ran here and passed all 87 of its checks.

What JSON GENERATE needs

JSON GENERATE arrived in the 3.x line; the 3.1 section of the release's NEWS file lists it with the note "runtime support needs additional library cJSON or JSON-C". The compiler always understands the statement. Whether a compiled program can run it depends on how your GnuCOBOL was built, and cobc --info tells you: it prints a line labelled JSON library, which the 3.2 build script fills with cjson, json-c, disabled or not found.

If your GnuCOBOL was built without either library, the 3.2 compiler refuses the program. Its message, "runtime is not configured to support JSON", belongs to a warning class that cobc treats as an error unless -Wno-unsupported silences it. Past that option, or with a runtime library built without JSON, the 3.2 source shows what happens: each JSON GENERATE leaves the receiving field alone, sets the JSON-CODE special register to 500 and raises a JSON exception, and the same message is printed once as a warning. A program that never looks at JSON-CODE would write nothing and give no reason.

The input record

The copybook, /code/cobol-json-generate-parse.cpy, describes a 91-byte account record:

Table 4.4-1
Field Bytes PICTURE Meaning
Acct-Id 1 to 8 X(8) account number
Acct-Holder 9 to 32 X(24) name of the holder
Acct-Joint 33 to 56 X(24) joint holder, spaces if there is none
Acct-Branch 57 to 60 9(4) branch code, with leading zeros
Acct-Balance 61 to 70 S9(7)V99 SIGN IS LEADING SEPARATE a + or - sign, then nine digits, two of them decimals
Acct-Opened 71 to 78 9(8) date opened, YYYYMMDD, also redefined as year, month and day
Acct-Status 79 X A active, D dormant, C closed
FILLER 80 to 82 X(3) unused
Acct-Tax-Ref 83 to 91 X(9) tax reference, which must not leave the bank

The eight records in /datasets/cobol-json-generate-parse.dat were chosen to hit the cases that trip JSON up: six accounts with no joint holder, a holder name with double quotes in it, a negative balance, balances of 0.05 and 0.00, a name that fills all 24 bytes, and branch codes such as 0007 and 0042. The separate sign keeps the file readable in an editor; the guide's SIGN IS section says the clause puts "an actual '+' or '-' character" into the field instead of folding the sign into a digit.

First attempt: straight from the copybook

The obvious program hands the record to JSON GENERATE as it is. The listing does that once, for the first record, and displays the result:

Show-Raw-Json.
    JSON GENERATE WS-Json FROM Acct-Rec
        COUNT IN WS-Json-Len
        SUPPRESS Acct-Tax-Ref
        ON EXCEPTION
            MOVE JSON-CODE TO WS-Show
            DISPLAY "Raw JSON GENERATE failed, JSON-CODE " WS-Show
            END-DISPLAY
        NOT ON EXCEPTION
            DISPLAY "Straight from the copybook:"
            DISPLAY WS-Json(1:WS-Json-Len)
    END-JSON.
Listing 4.4-1. Lines 116 to 127 of cobol-json-generate-parse.cob.

The record 10004217 Ada Lovell should come out as:

{"Acct-Rec":{"Acct-Id":"10004217","Acct-Holder":"Ada Lovell","Acct-Joint":" ","Acct-Branch":42,"Acct-Balance":1234.50,"Acct-Opened":20190314,"Acct-Status":"A"}}

Every JSON name is a data name, spelled as the copybook spells it, hyphens included. IBM's 6.4 Language Reference states the rule for its compiler on page 382: "The exact mixed-case spelling of data-names from the data description entry is retained." GnuCOBOL 3.2 gets the same result by a different route: it keeps each word as it was first written anywhere in the program, and a comment beside that code in the 3.2 source marks the arrangement as provisional, with an all-upper-case approach planned for later. Names a consumer depends on are safer pinned down with NAME OF, which the real conversion below does.

Acct-Opened-Parts is missing because it is a REDEFINES, and FILLER is missing because it has no name. Both compilers skip them. In GnuCOBOL the rule is one line in cobc/field.c, which ignores any item that is FILLER, a REDEFINES or a RENAMES; IBM's list on pages 372 to 373 says the same at more length. SUPPRESS Acct-Tax-Ref removed the tax reference by name.

The numbers are where a straight conversion goes wrong. Acct-Branch is PIC 9(4), so it became the JSON number 42, and branch 0042 lost the zeros that a consumer matching branch codes as text would need. Acct-Opened came out as the number 20190314, which is not a date to anything downstream. The balance kept both decimals, 1234.50. And the joint holder, 24 spaces, became a string holding one space. None of this is a defect. It is what the PICTURE clauses say, and the fix is to give JSON GENERATE a record whose PICTURE clauses say what the JSON should be.

A record shaped for JSON

The listing defines a second group for the JSON, fills it from each input record, and generates from that:

01  Json-Account.
    05  J-Id               PIC X(8).
    05  J-Holder           PIC X(24).
    05  J-Joint            PIC X(24).
    05  J-Branch           PIC X(4).
    05  J-Balance          PIC S9(7)V99.
    05  J-Opened           PIC X(10).
    05  J-Status           PIC X(7).

Write-Json.
    JSON GENERATE WS-Json FROM Json-Account
        COUNT IN WS-Json-Len
        NAME OF Json-Account IS OMITTED,
                J-Id         IS "accountId",
                J-Holder     IS "holder",
                J-Joint      IS "jointHolder",
                J-Branch     IS "branch",
                J-Balance    IS "balance",
                J-Opened     IS "opened",
                J-Status     IS "status"
        ON EXCEPTION
            ADD 1 TO WS-Failed
            MOVE JSON-CODE TO WS-Show
            DISPLAY "Record " WS-Read " not converted, JSON-CODE " WS-Show
            END-DISPLAY
        NOT ON EXCEPTION
            MOVE SPACES TO Json-Line
            MOVE WS-Json(1:WS-Json-Len) TO Json-Line
            WRITE Json-Line
            ADD 1 TO WS-Written
            ADD J-Balance TO WS-Total
            IF WS-Json-Len > WS-Longest
                MOVE WS-Json-Len TO WS-Longest
            END-IF
    END-JSON.

Each field's PICTURE now decides its JSON type on purpose. J-Branch is PIC X(4): a MOVE from the numeric Acct-Branch copies the four digits as characters, leading zeros included, which is the numeric-to-alphanumeric case in the MOVE truncation rules, and a PIC X field becomes a JSON string. J-Opened is built by STRING from the three parts of the REDEFINES as 2019-03-14. The guide's STRING section requires every sending item to be USAGE DISPLAY, which the three PIC 9 parts are, and notes that STRING does not space-fill its receiving field, which is why the program clears J-Opened first. The 88-level names on Acct-Status turn the letter into a word. J-Balance stays numeric, so the balance stays a JSON number.

NAME OF gives each member the name the consumer expects. GnuCOBOL 3.2 checks every NAME OF literal with the same routine it uses for XML names; for plain ASCII that means a letter, underscore or colon first, then letters, digits, hyphens, periods, underscores or colons. A JSON name with a space in it or a leading digit is legal JSON but a compile error here. NAME OF Json-Account IS OMITTED drops the outer name, so each line is a bare object rather than one wrapped in {"Json-Account":...}. IBM accepts OMITTED once the fix for APAR PH18641 is installed, according to page 376.

COUNT IN receives the length of the generated text, and the program writes exactly that much, WS-Json(1:WS-Json-Len).

One line in each ON EXCEPTION branch is there for a reason that is easy to miss. In GnuCOBOL, DISPLAY has ON EXCEPTION and NOT ON EXCEPTION phrases of its own; section 7.8.12.1 of the guide shows them, and the 3.2 grammar accepts them on an ordinary DISPLAY. Without END-DISPLAY, the NOT ON EXCEPTION that follows would belong to the DISPLAY, so the code that writes the line would run only after a failed JSON GENERATE, once the error message had been displayed, and a successful one would write nothing. GnuCOBOL's own JSON exception test closes its DISPLAY with END-DISPLAY in the same place. IBM has no such trap: its DISPLAY takes no exception phrases, and its reserved-word table lists END-DISPLAY only as a potential reserved word (page 766), so this program's END-DISPLAY is GnuCOBOL code.

Before converting a record, the main loop checks that Acct-Branch, Acct-Balance and Acct-Opened pass the NUMERIC class test, and skips the record if one does not; we did not trace what JSON GENERATE does with a non-digit in a numeric field. The guide's class-condition section says plus and minus signs fail NUMERIC, which is true of an unsigned field. For a signed DISPLAY field with SIGN SEPARATE, the 3.2 runtime's check (cob_check_numdisp in libcob/common.c) accepts a + or - in the sign position and requires digits everywhere else, so +000123450 passes.

What the program writes

The companion's expected standard output:

Straight from the copybook:
{"Acct-Rec":{"Acct-Id":"10004217","Acct-Holder":"Ada Lovell","Acct-Joint":" ","Acct-Branch":42,"Acct-Balance":1234.50,"Acct-Opened":20190314,"Acct-Status":"A"}}
Records read      :     8
JSON lines written:     8
Records failed    :     0
Longest line      :   159
Balance total     :        74,490.30

And accounts.jsonl:

{"accountId":"10004217","holder":"Ada Lovell","jointHolder":" ","branch":"0042","balance":1234.50,"opened":"2019-03-14","status":"active"}
{"accountId":"10004218","holder":"Mae \"Dot\" Fenwick","jointHolder":"Owen Fenwick","branch":"0042","balance":18750.00,"opened":"2016-08-22","status":"active"}
{"accountId":"10004219","holder":"Ravi Narayanan","jointHolder":" ","branch":"0107","balance":-75.25,"opened":"2021-01-05","status":"active"}
{"accountId":"10004220","holder":"Lucia Ortega Villanueva","jointHolder":" ","branch":"0007","balance":0.05,"opened":"2012-06-30","status":"dormant"}
{"accountId":"10004221","holder":"Tomasz Zielinski","jointHolder":"Ewa Zielinska","branch":"0215","balance":42100.99,"opened":"2008-09-11","status":"active"}
{"accountId":"10004222","holder":"Grace O'Neill","jointHolder":" ","branch":"0007","balance":0.00,"opened":"2023-04-17","status":"closed"}
{"accountId":"10004223","holder":"Kofi Mensah-Bonsu","jointHolder":" ","branch":"0315","balance":2500.00,"opened":"2025-09-02","status":"active"}
{"accountId":"10004224","holder":"Margaret Ashworth-Barnes","jointHolder":" ","branch":"0107","balance":9980.01,"opened":"2015-12-01","status":"active"}

Three checks are worth doing by eye. The balances in the JSON add up to 74,490.30, the total the file itself carries, and the companion asserts that after parsing every line back with Python's json module. No line contains the tax reference. And the longest line, 159 characters, is the account with the quoted nickname and a joint holder, comfortably inside the 300-byte receiving field. IBM's rule of thumb on page 372 is that the receiver "should be from 2 to 3 times the size of identifier-2". Json-Account is 86 bytes and the longest line is 1.85 times that, under the rule because the NAME OF names are short and trimming removes most of the padding.

The formatting rules, from the 3.2 source

Numbers. Inside the runtime, get_num in libcob/mlio.c moves a numeric field to a numeric-edited picture built for it: a floating minus sign over the integer digits, one 9 before the decimal point, then the decimal places. The leading spaces are trimmed. So leading zeros go, one digit always stays before the point, every decimal place is kept, and a sign appears only on negative values: 0.05 stays 0.05, zero with two decimals is 0.00, and -75.25 is -75.25. IBM's conversion and trimming rules on pages 381 to 382 give the same results, and its examples (-012.340 becomes -12.340, 0000.45 becomes 0.45, 0013 becomes 13, 0000 becomes 0) are among the companion's checks. The decimal point is a period even under DECIMAL-POINT IS COMMA, unless the dpc-in-data option is set to json or all; the 3.2 default is xml.

Text. A text field loses its trailing spaces, or its leading spaces if it is JUSTIFIED RIGHT, and a field that is all spaces keeps exactly one. GnuCOBOL's trimming routine does this, and IBM's page 382 says "If a character value consists solely of spaces, one space remains". That is why six lines carry "jointHolder":" ". A consumer that wants an empty string or a null has to map it; GnuCOBOL 3.2 cannot drop a field only when it is blank, as the next sections show.

Escaping. The JSON library does the escaping, and the two libraries GnuCOBOL can use do not quite agree. Both write a double quote as \", as line 2 shows, and a backslash as \\. cJSON writes a forward slash as it is. JSON-C, called with the plain formatting flag that GnuCOBOL 3.2 uses, writes it as \/ unless a separate no-slash-escape flag is set, which 3.2 does not set. Both forms are valid JSON and read back the same, but a test that compares files byte for byte should know which library its runtime has. The data here has no slash in it.

Layout. The output has no spaces or line breaks between members. IBM's reference says the same of its compiler on page 380, "No extra white space (new lines, indentation, and so forth) is inserted", and that is what lets one object sit on one line.

When the receiving field is too small

If the text does not fit, GnuCOBOL 3.2 copies as much as fits, sets JSON-CODE to 1 and raises the exception. The project's own test pins the details: generating {"valid-rec":{"a":"aa","b":"bb"}} into a five-byte field leaves {"val in it, and COUNT IN reports 33, the length of the whole text rather than the five characters that fit. In GnuCOBOL, then, COUNT IN after a failure is the size the field needed. IBM uses the same code, 1, but its page 379 says the content of the receiver "is undefined" and that COUNT IN holds "the number of character positions that were actually generated", so code that uses the partial text or the count after a failure is GnuCOBOL code.

Both failures this program can meet, a field too small (JSON-CODE 1) and a runtime without a JSON library (JSON-CODE 500), raise an exception in the EC-JSON class, which is the class GnuCOBOL's generated code tests for ON EXCEPTION. The program counts the record as failed, displays the code, and ends with return code 8.

What 3.2 cannot generate

Tables. The 3.2 compiler accepts a record with an OCCURS item but warns "OCCURS items in JSON/XML GENERATE is not implemented", and in the 3.2 test suite the JSON check that would generate a record holding a table is commented out, below the note "TO-DO: Add support for generating OCCURS items." IBM writes a table as a JSON array; its example on pages 373 to 374 produces "Stuff":[7.8,-9.0]. That gap is why this program writes one object per record instead of one array holding every account. JSON Lines needs no OCCURS at all, and a consumer that wants an array can wrap the lines in brackets.

Floating point. COMP-1 and COMP-2 fields draw a similar "not implemented" warning, and at run time the 3.2 library stops the program with "attempt to use non-implemented JSON I/O".

Conditional suppression, true, false and null. In 3.2 the SUPPRESS phrase of JSON GENERATE takes data names and nothing else. IBM's also takes WHEN ZERO, WHEN SPACE and the other figurative conditions (pages 371 and 376 to 377), and a CONVERTING phrase that writes a one-character flag as JSON true or false, or a blank field as null (pages 378 to 379). None of these is in the 3.2 grammar, so the blank joint holder stays " " unless the program runs a second JSON GENERATE with SUPPRESS J-Joint for the accounts that have none.

JSON PARSE, and the way back

Here the compiler is friendlier than the runtime. GnuCOBOL 3.2's grammar has a rule for JSON PARSE, so a program that uses it compiles. The rule issues a warning, "JSON PARSE is not implemented", from the class of warnings 3.2 always shows unless -Wno-pending turns it off, and emits no code for the statement. For a statement with ON EXCEPTION or NOT ON EXCEPTION phrases, cobc/codegen.c writes C that clears the exception code, runs the statement's own code, of which there is none, and then tests for an exception. None occurred, so the NOT ON EXCEPTION branch runs as if the parse had worked, with the receiving record untouched. The Programmer's Guide is accurate, if terse: its Appendix F1 list of compiler changes says "JSON PARSE statement: added recognition of".

On IBM Enterprise COBOL the statement exists. Reading one of these lines back into Json-Account would look like this; it follows the syntax diagram on page 383 of the 6.4 Language Reference and has not been compiled anywhere:

*> IBM Enterprise COBOL 6.4 only. GnuCOBOL 3.2 accepts it with a
*> warning and generates no parsing code for it.
JSON PARSE WS-Json(1:WS-Json-Len) INTO Json-Account
    NAME OF Json-Account IS OMITTED,
            J-Id         IS "accountId",
            J-Holder     IS "holder",
            J-Joint      IS "jointHolder",
            J-Branch     IS "branch",
            J-Balance    IS "balance",
            J-Opened     IS "opened",
            J-Status     IS "status"
    ON EXCEPTION
        DISPLAY "JSON PARSE failed, JSON-CODE " JSON-CODE
    NOT ON EXCEPTION
        IF JSON-STATUS NOT = ZERO
            DISPLAY "Parsed with conditions, JSON-STATUS " JSON-STATUS
        END-IF
END-JSON

Three rules from the reference shape what that statement does. Matching is by name: data names match JSON names regardless of case, but a NAME phrase literal "must match the JSON name exactly" (pages 388 and 393). A missing or extra name does not stop the statement; it sets a reason code in JSON-STATUS, 1 for a data item that found no value and 2 for a JSON value that found no data item (pages 393 to 394, with the full list of codes in Appendix D of the Programming Guide). And text in an alphanumeric field is assumed to be UTF-8 unless an ENCODING phrase says otherwise (page 385), which matters if the file was converted to EBCDIC on its way to the mainframe.

For GnuCOBOL 3.2 the way back is outside COBOL. The companion shows one route: it parses each line with Python's json module, reading numbers as decimals rather than floats, and rebuilds the 91-byte record. For all eight accounts, bytes 1 to 79 come back exactly, the joint holders padded back from one space to 24; bytes 80 to 91, the FILLER and the tax reference, were never in the JSON. Writing those records to a file and reading them with an ordinary sequential READ is the pattern from generating fixed-width test data with Python. The other route is a C JSON library called from COBOL, as in calling a C function from COBOL; we have not written or checked that version.

Where GnuCOBOL 3.2 and IBM differ

Table 4.4-2
Behaviour GnuCOBOL 3.2 (source and tests) IBM Enterprise COBOL 6.4 (page)
JSON PARSE compiles with "JSON PARSE is not implemented"; no parsing code implemented (383 to 395)
OCCURS in the source record warning, "not implemented"; the table test is commented out a JSON array (373 to 374)
COMP-1 and COMP-2 warning, then a run-time stop written as external floating point (381)
SUPPRESS data names only also WHEN conditions and EVERY NUMERIC or NONNUMERIC (371, 376 to 377)
true, false, null none CONVERTING and INDICATING phrases (374, 378 to 379)
NAME OF literal must be a valid XML 1.0 name any alphanumeric or national literal (376)
NAME OF ... IS OMITTED supported with the fix for APAR PH18641 (376)
Default JSON names each data name as first written in the program the spelling in the data description entry (382)
Receiver longer than the text rest filled with spaces rest left as it was (380)
Receiver too small JSON-CODE 1, the part that fits is kept, COUNT IN is the full length JSON-CODE 1, receiver undefined, COUNT IN is the positions generated (379)
No JSON library at compile time an error unless -Wno-unsupported not applicable
No JSON library in the runtime JSON-CODE 500 and a warning not applicable
Character conversion none; bytes are copied as they are converted to UTF-8 under the CODEPAGE option (372)
END-DISPLAY before NOT ON EXCEPTION needed only a potential reserved word (766)

Compile and run

cobc --info
cobc -x -I code code/cobol-json-generate-parse.cob
./cobol-json-generate-parse
python code/cobol-json-generate-parse.py accounts.jsonl

Check the JSON library line of cobc --info first. Run the rest from the site folder, since the program opens its data file by a relative path. With a file name, the companion compares the file the program wrote with the expected lines, prints any line that differs, and runs the way back on what it found; with no argument it prints the expected output and its own checks.

Limitations

  • Not compiled, not run. The expected output is the companion's, built from our reading of the 3.2 source. It reproduces the expected results in the 3.2 JSON tests and IBM's trimming examples before it is applied to the data, but a real run is the test that counts. If your GnuCOBOL disagrees, the compiler wins, and the compare mode will show where.
  • Two libraries. The expected bytes are what cJSON and JSON-C both write for this data. Data with a forward slash in it would make a byte comparison depend on the library.
  • No character conversion. GnuCOBOL 3.2 copies each field's bytes into the JSON as they are, so the data must already be UTF-8 for the output to be valid JSON. This file is plain ASCII; a name with an accent stored in Latin-1 would not be.
  • Release scope. We read the 3.2 release, the newest on the GNU project's server on October 5, 2026, not the development branches. The source's own comment on default names says that part may change, and NAME OF keeps this program's output stable either way.
  • The IBM statement. The JSON PARSE example follows the 6.4 reference and has not been compiled; there is no z/OS system here to try it on.

Index terms cobol, gnucobol, json, json generate, json parse