Specification: Ballerina EDI Library

Owners: @chathurace @RDPerera
Reviewers: @niveathika @chathurace
Created: 2024/01/19
Updated: 2026/08/20
Edition: Swan Lake

Introduction

This is the specification for the EDI library of the Ballerina language. The library converts EDI text into JSON or typed Ballerina records and back, against a schema written in JSON. It reads EDI at four depths: envelope headers without a schema, envelope headers with a schema, a full interchange, and a single transaction body.

This specification may change in future versions. Released versions can be found under the matching GitHub tag.

If you have feedback or suggestions, start a discussion with a GitHub issue or in the Discord server. The specification and the implementation can then be updated together. Any accepted proposal that affects this specification is stored under /docs/proposals; proposals under discussion carry the type/proposal label.

The implementation that matches this specification is released with the library. Anything the library does differently from this document is a bug.

Contents

  1. Overview
  2. Concepts
  3. Errors
  4. Reading EDI
  5. Writing EDI
  6. Envelope processing
  7. Schema definition

1. Overview

An EDI document is text, structured by delimiters and described by a schema. This library reads and writes that text at the depth the caller asks for. The four read depths differ in whether they need a schema, how much of the document they touch, and how they behave on malformed input.

FunctionSchemaReadsOn malformed input
x12HeadersFromEdiString, x12HeadersFromEdiFileNot neededX12 ISA, and GS when presentFails
edifactHeadersFromEdiString, edifactHeadersFromEdiFileNot neededEDIFACT UNB, and UNH when presentFails
headersFromEdiString, headersFromEdiFileWith envelopeThe envelope header segmentsFails
interchangeFromEdiStringWith envelopeThe whole interchangeFails on the envelope, captures the error per transaction body
fromEdiStringAnyOne transaction bodyFails

Two functions write EDI text: toEdiString writes a transaction body, and interchangeToEdiString writes a whole interchange. getSchema turns a JSON schema into an EdiSchema.

The four schema-free functions know the X12 and EDIFACT envelope structures; every other function reads the structure from the schema it is given.

2. Concepts

2.1. The EDI hierarchy

An EDI file is a nested structure:

EDI file
└── Interchange envelope (ISA / IEA, UNB / UNZ)
    └── Functional group (X12 only — GS / GE)
        └── Transaction (ST / SE, UNH / UNT)
            └── Segment (header, items, summary…)
                └── Field
                    └── Component
                        └── Sub-component

A segment is one line of related data elements, identified by a code. A field holds one data element. A field may hold components, and a component may hold sub-components.

The interchange, group, and transaction levels are the envelope. The segments inside a transaction are the body. A schema describes the body in segments and the envelope in envelope.

Functional groups exist in X12 and in EDIFACT documents that use UNG / UNE. An EDIFACT document without them has interchange and transaction levels only.

2.2. Schema

A schema is JSON, defined in Section 7. getSchema accepts it as a string or as json and returns an EdiSchema, or an error when the schema is not valid. Segment references are resolved at this point, so the parser never sees an unresolved ref.

An EdiSchema is the first argument every schema-driven function takes.

2.3. Envelope

A schema either declares an envelope or it does not.

With envelope, the envelope-aware functions — headersFromEdiString, headersFromEdiFile, interchangeFromEdiString, and interchangeToEdiString — are available, and fromEdiString skips the envelope segments and reads only the body.

Without envelope, fromEdiString reads every segment in segments, and the envelope-aware functions return a SchemaCompatibilityError. Schemas generated before envelope existed fall into this case; regenerating them with edi-tools 2.2.0 or later adds it.

Envelope header and trailer segments are always mandatory to the envelope-aware functions, whatever minOccurances the schema declares for them.

3. Errors

Every function in this library returns either edi:Error or one of the types below it.

public type Error distinct error;

public type InvalidEnvelopeError distinct Error;
public type SchemaCompatibilityError distinct Error;
public type SerializationError distinct Error;
ErrorWhen you get it
InvalidEnvelopeErrorThe EDI text does not match the expected envelope structure
SchemaCompatibilityErrorThe schema cannot support the operation asked of it
SerializationErrorinterchangeToEdiString refuses to write the interchange it was given

InvalidEnvelopeError covers: a mandatory envelope segment missing or not matching; content left after the interchange trailer, or a second interchange header inside the body (Section 6.2); an X12 ISA that is malformed, truncated, or not the standard fixed width; a UNA whose delimiters conflict with the schema (Section 6.3); a multi-transaction interchange passed to fromEdiString; and envelope headers that do not fit the read window of headersFromEdiFile.

SchemaCompatibilityError covers: a schema without envelope passed to an envelope-aware function; a fixed-length schema used with an envelope-aware function (Section 6.5); and a segment reference that reaches the runtime unresolved inside an envelope section.

SerializationError covers: a transaction whose body holds an error; a body or envelope section that is not a JSON object; and a missing groups or transactions field, whichever the schema's envelope shape requires.

Body parsing for schemas without an envelope returns the generic edi:Error.

4. Reading EDI

4.1. Envelope headers without a schema

x12HeadersFromEdiString and edifactHeadersFromEdiString read the interchange header, and the group or message header that follows it, from EDI text. They take no schema. x12HeadersFromEdiFile and edifactHeadersFromEdiFile do the same for a file, reading only its first 512 characters, which holds any conforming header pair.

public type X12Headers record {|
    X12ISA isa;
    X12GS gs?;
|};

public type EdifactHeaders record {|
    EdifactUNB unb;
    EdifactUNH unh?;
|};

isa and unb are always present. gs is present when a GS segment follows the ISA, and unh when a UNH follows the UNB.

The X12 ISA is fixed-width, 106 characters. It is validated against the standard element widths ISA01 to ISA16, and an ISA that does not conform — an unpadded one, for instance — is rejected with InvalidEnvelopeError rather than partly parsed.

The EDIFACT functions honour a UNA service string advice in full, taking all six service characters from it (Section 6.3). Splitting is release-character aware: a delimiter escaped by the release character is data, and release sequences are un-escaped in the returned values, so ?+ becomes +, ?: becomes :, ?' becomes ', and ?? becomes ?.

The record fields are:

public type X12ISA record {|
    string authInfoQualifier; string authInfo;
    string securityQualifier; string securityInfo;
    string senderQualifier; string senderId;
    string receiverQualifier; string receiverId;
    string date; string time;
    string version; string controlNumber; string usageIndicator;
|};

public type X12GS record {|
    string functionalIdentifier;
    string senderId; string receiverId;
    string date; string time;
    string controlNumber; string version;
|};

EDIFACT composites inside UNB and UNH are separate records.

public type EdifactSyntaxIdentifier record {|
    string syntaxId;
    string syntaxVersion;
|};

public type EdifactInterchangeParty record {|
    string id;
    string qualifier;
|};

public type EdifactDateTime record {|
    string date;
    string time;
|};

public type EdifactUNB record {|
    EdifactSyntaxIdentifier syntaxIdentifier;
    EdifactInterchangeParty sender;
    EdifactInterchangeParty recipient;
    EdifactDateTime dateAndTime;
    string controlRef;
|};

public type EdifactMessageIdentifier record {|
    string messageType;
    string version;
    string release;
    string controlAgency;
|};

public type EdifactUNH record {|
    string messageRef;
    EdifactMessageIdentifier messageIdentifier;
|};

4.2. Envelope headers with a schema

headersFromEdiString reads the envelope header segments a schema declares — interchange, group when the schema has one, and transaction — and stops there. The rest of the document is never read. The result is a JSON map with an interchange entry, a group entry when the schema declares a group level, and a transaction entry.

Input that does not match the declared headers fails with InvalidEnvelopeError; the function never returns empty header sections.

headersFromEdiFile does the same for a file, reading its first 4096 characters. When the headers cannot be read and that whole window was consumed, the returned InvalidEnvelopeError names the window size, since the header section may be longer than the window.

4.3. A full interchange

interchangeFromEdiString reads a whole interchange into an EdiInterchange.

public type EdiInterchange record {|
    json interchangeHeader;
    EdiFunctionalGroup[] groups?;
    EdiTransaction[] transactions?;
    json interchangeTrailer;
|};

public type EdiFunctionalGroup record {|
    json groupHeader;
    EdiTransaction[] transactions;
    json groupTrailer;
|};

public type EdiTransaction record {|
    json transactionHeader;
    json|error body;
    json transactionTrailer;
|};

groups is set when the schema declares envelope.group, and transactions is set when it does not.

Envelope segments fail the call: a malformed or missing interchange, group, or transaction header or trailer returns InvalidEnvelopeError.

Transaction bodies do not. When a body cannot be read against schema.segments, that transaction's body field holds the parse error and the rest of the interchange is still returned. body is json|error for this reason, and the error carries the message describing what failed.

Trailer counts and control references are captured as they appear in the input. They are not checked against the content (Section 6.1).

4.4. A transaction body

fromEdiString reads one transaction body and returns it as json.

For a schema declaring an envelope, envelope segments are skipped by position: header segments at the start of the input, trailer segments at the end. An envelope-coded segment anywhere else is not removed and is reported as a body parse error. The input must hold at most one transaction; more than one transaction header segment returns InvalidEnvelopeError naming interchangeFromEdiString as the function to use instead.

For a schema without an envelope, every segment is read against segments.

5. Writing EDI

5.1. A transaction body

toEdiString writes json as EDI text against schema.segments. It writes the body only, and does so even when the schema declares an envelope — no envelope segments are emitted.

5.2. A full interchange

interchangeToEdiString writes an EdiInterchange as EDI text, and is the inverse of interchangeFromEdiString. Interchange, group, and transaction headers and trailers come from the matching EdiInterchange fields; each transaction body is written against schema.segments, the same fragment fromEdiString reads. A read followed by a write is therefore structurally symmetric.

Two values are adjusted on write, rather than taken from the record: the X12 ISA is re-padded to its standard element widths, and trailer counts and control references are recomputed (Section 6.1).

The call returns SerializationError when a transaction body holds an error, when a body or envelope section is not a JSON object, or when the groups field is unset for a schema with a group level, or transactions unset for one without.

6. Envelope processing

6.1. Counts and control references

Trailer counts (SE01, GE01, IEA01, UNT01, UNZ01) and trailer control references (SE02, GE02, IEA02, UNT02, UNZ02) are not validated on read. interchangeFromEdiString captures whatever the input holds.

interchangeToEdiString recomputes them from the content it is writing, so values captured at read time — stale after the caller edits the transaction list — are ignored:

  • transaction trailer count (SE01, UNT01) is the number of segments in the transaction, counting the transaction header and trailer;
  • group trailer count (GE01) is the number of transaction sets in the group;
  • interchange trailer count (IEA01, UNZ01) is the number of functional groups, or the number of messages when the schema has no group level;
  • trailer control references are mirrored from the matching header: IEA02 from ISA13, GE02 from GS06, SE02 from ST02, UNT02 from UNH 0062, UNZ02 from UNB 0020.

These elements are identified by position, per the standard segment layouts: the count is the first element after the segment code, and the control reference is the element after the count. When the schema declares a trailer with fewer fields, only what fits is written.

The X12 ISA is re-padded on write to the standard element widths — ISA01 to ISA16 are 2, 10, 2, 10, 2, 15, 2, 15, 6, 4, 1, 5, 9, 1, 1, and 1 character — producing the mandatory 106-character segment. Reading trims this padding; receivers read the ISA by position. A fixed field length declared in the schema takes precedence over the standard width.

6.2. One interchange per call

interchangeFromEdiString reads exactly one interchange. Content after the interchange trailer, or a second interchange header segment inside the body, returns InvalidEnvelopeError. Batched input must be split into single interchanges first.

fromEdiString with an envelope schema reads exactly one transaction body, per Section 4.4.

6.3. UNA service string advice

The schema-free EDIFACT functions honour a UNA in full. All six service characters — component separator, field separator, decimal notation, release character, reserved character, and segment terminator — are taken from it, including custom sets.

The schema-driven functions validate a leading UNA against the schema delimiters: component separator, field separator, decimal separator when the schema declares one, and segment terminator. A matching UNA is skipped. A conflicting one returns InvalidEnvelopeError, since a schema-driven parse cannot use delimiters other than the schema's.

6.4. Trailer location

interchangeFromEdiString finds the interchange trailer by scanning backward from the end of the input, and finds group and transaction trailers by scanning backward from the next header at the same level.

A trailer-coded segment inside a corrupted transaction body therefore stays in that body, where it is captured as that transaction's error, and cannot be mistaken for the envelope trailer.

6.5. Fixed-length schemas

The envelope-aware functions extract segment codes by delimiter and do not support fixed-length schemas — those declaring "field": "FL". They return SchemaCompatibilityError, and so does fromEdiString when a fixed-length schema declares an envelope.

fromEdiString and toEdiString with a fixed-length schema that has no envelope are unaffected.

6.6. Byte order marks

A single leading U+FEFF is stripped by the string and file entry points of the envelope-aware functions before the envelope is detected.

7. Schema definition

A schema is a JSON object. It names the format, states its delimiters, and describes the transaction body in segments and, optionally, the envelope in envelope.

This schema describes a custom order format with one mandatory header segment and a repeating item segment:

{
    "name": "SimpleOrder",
    "delimiters": {"segment": "~", "field": "*", "component": ":", "repetition": "^"},
    "segments": [
        {
            "code": "HDR",
            "tag": "header",
            "minOccurances": 1,
            "fields": [
                {"tag": "code"},
                {"tag": "orderId"},
                {"tag": "organization"},
                {"tag": "date"}
            ]
        },
        {
            "code": "ITM",
            "tag": "items",
            "maxOccurances": -1,
            "fields": [
                {"tag": "code"},
                {"tag": "item"},
                {"tag": "quantity", "dataType": "int"}
            ]
        }
    ]
}

It reads this EDI text:

HDR*ORDER_1201*ABC_Store*2008-01-01~
ITM*A-250*12~
ITM*A-45*100~

into this JSON:

{
    "header": {"code": "HDR", "orderId": "ORDER_1201", "organization": "ABC_Store", "date": "2008-01-01"},
    "items": [
        {"code": "ITM", "item": "A-250", "quantity": 12},
        {"code": "ITM", "item": "A-45", "quantity": 100}
    ]
}

7.1. Name and root tag

FieldDefaultMeaning
nameName of the schema. Code generation names the top-level record after it
tag"Root_mapping"Tag of the schema's root segment group. fromEdiString returns the segment map directly, so this does not appear in the parsed output

7.2. Delimiters

delimiters states the characters that separate each level of the EDI text.

FieldDefaultMeaning
segmentSeparates segments, such as ~, ', or a newline
fieldSeparates fields within a segment, such as * or +. "FL" marks a fixed-length format, where fields are located by startIndex and length instead
componentSeparates components within a field, such as :
subcomponent"NOT_USED"Separates sub-components. "NOT_USED" means the format has none
repetition"NOT_USED"Separates repetitions of a field. "NOT_USED" means the format has none
decimalSeparator.Decimal separator in numeric fields. EDIFACT uses .; some regional X12 flavours use ,
"delimiters": {
    "segment": "~",
    "field": "*",
    "component": ":",
    "subcomponent": "NOT_USED",
    "repetition": "^",
    "decimalSeparator": "."
}

7.3. Segments

segments is an array describing the transaction body. Each entry is a segment, a segment group, or a reference. Envelope segments belong in envelope, not here.

7.3.1. Segment

FieldDefaultMeaning
codeSegment code as it appears in the EDI text, such as HDR or BGM
tagTag for the segment. Becomes the JSON or record field name
minOccurances0Minimum occurrences
maxOccurances1Maximum occurrences. -1 means unlimited
truncatabletrueWhether trailing fields may be absent from the input, provided every required field before them is present
fieldsField definitions within the segment
{
    "code": "HDR",
    "tag": "header",
    "minOccurances": 1,
    "maxOccurances": 1,
    "truncatable": true,
    "fields": [
        {"tag": "code", "required": true},
        {"tag": "orderId", "required": true},
        {"tag": "organization"},
        {"tag": "date"}
    ]
}

7.3.2. Segment group

A segment group holds segments that appear together, as X12 loops and EDIFACT message branches do. An entry is a group when it has segments instead of fields.

FieldDefaultMeaning
tagTag for the group in the parsed output
minOccurances0Minimum occurrences
maxOccurances1Maximum occurrences. -1 means unlimited
segmentsNested segments, groups, or references. The first child must be a segment, which triggers the group
{
    "tag": "Loop_2000A",
    "minOccurances": 1,
    "maxOccurances": -1,
    "segments": [
        {"code": "HL", "tag": "hierarchicalLevel", "fields": []},
        {"code": "PRV", "tag": "providerCharacteristics", "minOccurances": 0, "fields": []}
    ]
}

7.3.3. Segment reference

A segment definition used at several points in a schema is declared once in segmentDefinitions and referenced by name from segments or from any envelope level.

FieldMeaning
refKey into segmentDefinitions
tagOptional. Overrides the tag of the referenced segment at this site
minOccurances, maxOccurancesOptional. Override the cardinality at this site
"segmentDefinitions": {
    "DTM": {
        "code": "DTM",
        "tag": "dateTimeReference",
        "fields": [{"tag": "code"}, {"tag": "dateTime"}]
    }
},
"segments": [
    {"ref": "DTM", "minOccurances": 1, "maxOccurances": 5}
]

References are resolved by getSchema before parsing.

7.4. Fields

FieldDefaultMeaning
tagTag for the field
repeatfalseWhether the field may repeat, using delimiters.repetition
requiredfalseWhether the field is required
truncatabletrueWhether trailing components within the field may be absent
dataType"string"string, int, float, or composite
startIndex-1Start index of the field within the segment. Fixed-length formats only
length-1Fixed length, or a {"min": N, "max": M} range
valuesLegal codes of the field (Section 7.4.3)
discriminatorCodes that identify this definition during segment matching (Section 7.4.3)
componentsComponent definitions, when dataType is composite

7.4.1. Data types

string is textual data. int is integer data. float is floating-point data and honours delimiters.decimalSeparator. composite is a group of components within the field, and each component may hold sub-components.

"fields": [
    {"tag": "CustomerName", "dataType": "string", "length": 50},
    {"tag": "Quantity", "dataType": "int", "length": {"min": 1}},
    {"tag": "Price", "dataType": "float", "length": {"max": 10}},
    {"tag": "Address", "dataType": "composite", "components": [
        {"tag": "No"},
        {"tag": "Street"},
        {"tag": "City"}
    ]}
]

7.4.2. Length constraints

When length is an integer, a value of that length is kept as is, a shorter value is padded with spaces, and a longer value is an error.

When length is a {"min": …, "max": …} object, a value below min or above max is an error.

"fields": [
    {"tag": "DocumentNameCode", "length": 10},
    {"tag": "DocumentNumber", "length": {"min": 1}},
    {"tag": "MessageFunction", "length": {"max": 3}},
    {"tag": "ResponseType", "length": {"min": 1, "max": 3}}
]

7.4.3. Value constraints and qualifier-based discrimination

EDI formats reuse one segment code for definitions with different meanings and rely on a qualifier value to identify each one. An X12 834 member loop defines a subscriber identifier (qualifier 0F), an optional member policy number (qualifier 1L), and supplemental identifiers (qualifiers 17, 23, DX) — all with segment code REF. Fields, components, and sub-components can declare such value sets with two attributes:

FieldDefaultMeaning
valuesThe legal codes of the element. Validated when writing EDI; never affects segment matching
discriminatorThe codes that identify this definition. Used for segment matching, and validated when writing EDI

Because values never affects matching, tools can attach full standard code lists without changing how existing messages parse; routing happens only where a discriminator is declared. An element may declare both: values records the element's full legal code list (for example the standard's code list for the data element) while discriminator records the narrower set an implementation guide permits at this position.

Matching semantics:

  • An input segment is an instance of a definition only when the segment code matches and every discriminator element's value is contained in its discriminator set.
  • A missing or empty discriminator value never matches — a segment that does not carry its identity cannot claim a discriminated definition.
  • When a segment matches no definition at the current schema position, parsing fails with an error naming the segment. An optional discriminated definition is correctly recognized as absent and skipped.
  • A run of consecutive definitions sharing one segment code, each declaring at least one discriminator, is matched as an unordered set: while input segments carry the run's code, every member that can still accept an occurrence is tried in schema order, so occurrences may arrive in any order and interleave freely. The run is left when a segment with a different code arrives or matches no member; on exit, every mandatory member must have at least one occurrence.
  • When several definitions could match, the first definition in schema order wins. The schema loader logs warnings when sibling definitions sharing a segment code have overlapping discriminator value sets.

Rules enforced when the schema is loaded:

  • A discriminator must list at least one code and must not be placed on a repeating field, nor on any component or sub-component of one: repetitions are position-insignificant, so nothing inside a repeating field can identify a definition.
  • A sub-component discriminator requires delimiters.subcomponent to be configured; without it a sub-component cannot be isolated from the component text around it.
  • When an element declares both attributes, every discriminator code must also appear in values — a definition that requires a code the element does not permit could never match.
  • Both attributes belong on the element that actually holds the value: on a component rather than the composite field around it, and on a sub-component rather than the component around it. values and discriminator on composite nodes are rejected.
  • Definitions sharing a segment code within one segment list must either all declare discriminators or none. A mix is rejected, because a code-only sibling would capture the segments its discriminated siblings rejected.

Example — the X12 834 member-level REF definitions:

{
    "code": "REF",
    "tag": "MemberPolicyNumber",
    "minOccurances": 0,
    "fields": [
        {"tag": "code"},
        {"tag": "qualifier", "required": true, "discriminator": ["1L"]},
        {"tag": "identifier", "required": true}
    ]
}

With this definition, REF*17*BARGAINED~ no longer matches MemberPolicyNumber (17 is not in {1L}), so the optional definition is skipped and the segment falls through to the definition whose value set contains 17.

Example — an EDIFACT RFF qualifier lives inside the C506 composite and is discriminated at component level. Combined with segment references, definitions sharing a code can be specialized per position:

"segmentDefinitions": {
    "RFF_VatNumber": {
        "code": "RFF",
        "tag": "VatNumber",
        "fields": [
            {"tag": "code"},
            {"tag": "REFERENCE", "required": true, "components": [
                {"tag": "qualifier", "required": true, "discriminator": ["VA"]},
                {"tag": "number", "required": true}
            ]}
        ]
    }
},
"segments": [
    {"ref": "RFF_VatNumber", "minOccurances": 0}
]

7.5. Components and sub-components

A component takes tag, required (default false), truncatable (default true), dataType (default "string"), values and discriminator (Section 7.4.3), and subcomponents. A sub-component takes tag, required (default false), dataType (default "string"), and likewise values and discriminator.

{
    "code": "ORG",
    "tag": "organization",
    "fields": [
        {"tag": "code"}, {"tag": "partnerCode"}, {"tag": "name"},
        {
            "tag": "contact",
            "components": [
                {"tag": "mobile", "required": true},
                {"tag": "fixedLine"},
                {
                    "tag": "address",
                    "subcomponents": [
                        {"tag": "streetAddress"},
                        {"tag": "city"},
                        {"tag": "country"}
                    ]
                }
            ]
        }
    ]
}

7.6. Envelope

envelope describes the interchange, group, and transaction levels, separately from the body segments. Section 2.3 states what declaring it changes.

LevelPresenceSegments
interchangeRequiredISA / IEA for X12, UNB / UNZ for EDIFACT
groupOptional. Present for X12, absent for EDIFACT without UNGGS / GE
transactionRequiredST / SE for X12, UNH / UNT for EDIFACT

Each level has a header and a trailer array, holding the same kinds of entries as segments: segments, groups, and references.

X12, with all three levels:

"envelope": {
    "interchange": {
        "header": [{"code": "ISA", "tag": "InterchangeControlHeader", "fields": []}],
        "trailer": [{"code": "IEA", "tag": "InterchangeControlTrailer", "fields": []}]
    },
    "group": {
        "header": [{"code": "GS", "tag": "FunctionalGroupHeader", "fields": []}],
        "trailer": [{"code": "GE", "tag": "FunctionalGroupTrailer", "fields": []}]
    },
    "transaction": {
        "header": [{"code": "ST", "tag": "TransactionSetHeader", "fields": []}],
        "trailer": [{"code": "SE", "tag": "TransactionSetTrailer", "fields": []}]
    }
}

EDIFACT without groups, with two levels:

"envelope": {
    "interchange": {
        "header": [{"code": "UNB", "tag": "InterchangeHeader", "fields": []}],
        "trailer": [{"code": "UNZ", "tag": "InterchangeTrailer", "fields": []}]
    },
    "transaction": {
        "header": [{"code": "UNH", "tag": "MessageHeader", "fields": []}],
        "trailer": [{"code": "UNT", "tag": "MessageTrailer", "fields": []}]
    }
}

7.7. Additional configuration

FieldDefaultMeaning
ignoreSegments[]Segment codes to skip while reading the body. Schemas written before envelope existed used it to suppress envelope segments
preserveEmptyFieldstrueWhether empty optional fields appear in the output as empty strings, nulls, and empty arrays. When false, they are left out
includeSegmentCodetrueWhether the segment code appears in the output as a code field
segmentDefinitionsReusable segment definitions, keyed by name and referenced by {"ref": "…"} from segments or any envelope level

A schema using a body, an envelope, and reusable definitions together:

{
    "name": "OrdersD03A",
    "tag": "Orders",
    "delimiters": {
        "segment": "'",
        "field": "+",
        "component": ":",
        "subcomponent": "NOT_USED",
        "repetition": "*",
        "decimalSeparator": "."
    },
    "ignoreSegments": [],
    "preserveEmptyFields": true,
    "includeSegmentCode": true,
    "envelope": {
        "interchange": {
            "header": [{"ref": "UNB"}],
            "trailer": [{"ref": "UNZ"}]
        },
        "transaction": {
            "header": [{"ref": "UNH"}],
            "trailer": [{"ref": "UNT"}]
        }
    },
    "segments": [
        {"code": "BGM", "tag": "BeginningOfMessage", "minOccurances": 1, "fields": []},
        {"code": "DTM", "tag": "DateTime", "maxOccurances": 5, "fields": []}
    ],
    "segmentDefinitions": {
        "UNB": {"code": "UNB", "tag": "InterchangeHeader", "fields": []},
        "UNZ": {"code": "UNZ", "tag": "InterchangeTrailer", "fields": []},
        "UNH": {"code": "UNH", "tag": "MessageHeader", "fields": []},
        "UNT": {"code": "UNT", "tag": "MessageTrailer", "fields": []}
    }
}