EDIFACT Tutorial - Parse, route, and serialize EDI messages

Parse EDIFACT interchanges into Python objects, route messages by type and serialize them back byte-for-byte.

EDIFACT is the EDI standard that connects trading partners in commerce, logistics, customs, and healthcare. Zato parses EDIFACT interchanges into Python objects, routes messages by type, and serializes them back byte-for-byte.

Parse and process your first EDIFACT interchange in under 5 minutes.

The examples in this tutorial use standard UN/EDIFACT supply chain messages - a bookshop ordering titles from a supplier. Every message type works the same way, whether it comes from the standard directories or an industry-specific dialect.

In this tutorial

  1. Parse your first interchange
  2. Navigate messages by segment
  3. Route by message type
  4. Serialize byte-for-byte
  5. Convert between formats
  6. Typed access with dialects

Remember: you can connect your AI copilot to Zato documentation.

Parse your first interchange

Step 1. If you do not have Zato running yet, install it via Docker - it takes under 5 minutes.

Step 2. Open the Zato IDE at http://localhost:8183, create a new file called edifact_api.py, paste this code, and click Deploy:

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service
from zato.edifact import parse_edifact

# ################################################################################################################################
# ################################################################################################################################

class MessageHandler(Service):
    """ Processes incoming EDIFACT interchanges.
    """
    name = 'edifact-api.message-handler'

    def handle(self) -> 'None':

        # The raw interchange, as delivered by any channel -
        # REST, file transfer, a mailbox poller or a test invocation
        raw = self.request.input

        # One call parses the envelope and all the messages in it
        interchange = parse_edifact(raw)

        # The UNB header identifies the trading partners
        sender = interchange.header.sender.identification

        for msg in interchange.messages:

            # The UNH message header holds the type identifier, e.g. ORDERS:D:03B:UN
            unh = msg.segments('UNH')[0]
            message_type = unh.e_2[0]

            self.logger.info('Received %s from %s', message_type, sender)

Step 3. Invoke the service with a test interchange - a purchase order in which a bookshop's supplier is asked for four Robert Louis Stevenson titles:

raw = (
    "UNB+UNOA:4+ORDERAPP:1+BOOKSHOP:1+20070312:1042+7104'\n"
    "UNH+MSGA1+ORDERS:D:03B:UN:EAN008'\n"
    "BGM+220+PORD42+9'\n"
    "DTM+137:20070312:102'\n"
    "NAD+BY+5410738000251::9'\n"
    "NAD+SU+4021456000082::9'\n"
    "LIN+1+1+0140329560:IB'\n"
    "QTY+1:30'\n"
    "FTX+AFM+1++Treasure Island'\n"
    "LIN+2+1+0140383441:IB'\n"
    "QTY+1:20'\n"
    "FTX+AFM+1++Kidnapped'\n"
    "UNT+12+MSGA1'\n"
    "UNZ+1+7104'"
)

response = self.invoke('edifact-api.message-handler', raw)

The log confirms the parse:

INFO - Received ORDERS from ORDERAPP

Note what just happened - the UNA-less interchange used default separators, the UNB envelope resolved both trading partners, and the message type came straight from the ORDERS:D:03B:UN:EAN008 identity in UNH.

Any message parses without prior configuration - no message definitions, no schemas to install. Segments are reachable by tag, and their elements by position:

msg = interchange.message

# The document number from BGM - e_2 is the second element
bgm = msg.segments('BGM')[0]
order_number = bgm.e_2                     # PORD42

# Repeating segments come back as lists, in wire order
for line_item in msg.segments('LIN'):

    # A multi-component element is a list of its components
    item_number = line_item.e_3            # ['0140329560', 'IB']

    self.logger.info('Line %s: %s', line_item.e_1, item_number[0])

Elements with a single component collapse to their scalar value, so bgm.e_2 is the string PORD42, while line_item.e_3 keeps both the item number and its IB (ISBN) qualifier.

The service segments of the envelope are fully typed out of the box:

# UNB - the interchange header
sender    = interchange.header.sender.identification       # ORDERAPP
recipient = interchange.header.recipient.identification    # BOOKSHOP
prepared  = interchange.header.prepared_at.date            # 20070312

# UNH - the message header
unh = msg.segments('UNH')[0]
reference = unh.e_1                                        # MSGA1

The full access model is described in EDIFACT field access.

Route by message type

An interchange can include different message types - orders, despatch advices, invoices - and the UNH identity tells you which is which, so route on it:

class Router(Service):
    """ Routes each message of an interchange to its handler.
    """
    name = 'edifact-api.router'

    handlers = {
        'ORDERS': 'edifact-api.handle-order',
        'DESADV': 'edifact-api.handle-despatch-advice',
        'INVOIC': 'edifact-api.handle-invoice',
    }

    def handle(self) -> 'None':

        interchange = parse_edifact(self.request.input)

        for msg in interchange.messages:

            unh = msg.segments('UNH')[0]
            message_type = unh.e_2[0]

            if message_type in self.handlers:
                service_name = self.handlers[message_type]
                self.invoke(service_name, msg=msg)
            else:
                # Unknown types are still parsed into fully navigable messages
                self.logger.info('Skipping %s', message_type)

Serialize byte-for-byte

Parsed interchanges serialize back exactly as they arrived, which makes pass-through, store-and-forward, and audit flows safe:

interchange = parse_edifact(raw)

# The wire text, reproduced exactly, one segment per line
wire = interchange.serialize()

# A second parse-serialize cycle is byte-stable
assert parse_edifact(wire).serialize() == wire

Escape sequences, custom separators from UNA, and repetition characters all survive the round trip untouched.

Convert between formats

Interchanges, messages, and segments convert to dicts and JSON - handy for handing EDIFACT content to REST APIs, queues, or storage:

interchange = parse_edifact(raw)

data = interchange.to_dict()
json_text = interchange.to_json(indent=2)

# Skip empty elements for compact output
compact = interchange.to_json(include_empty=False)

Each message's dictionary records its type under _message_type and its segments as positional dictionaries, so downstream consumers never need to know EDIFACT syntax.

Typed access with dialects

When you work with the same message types every day, positional access like e_3 gives way to named fields. A dialect is a Python package that declares message classes with named segments, elements, and components:

# Importing a dialect package registers its message classes -
# here, the Dutch healthcare dialect shipped as an example
import zato.edifact.nl

interchange = parse_edifact(raw)
msg = interchange.message

# Now fields have names instead of positions
family_name = msg.pid.patient_name.married_name
born_year   = msg.pid.date_of_birth.year

Read more:

Where to go next



Schedule a meaningful demo

Book a demo with an expert who will help you build meaningful systems that match your ambitions

"For me, Zato Source is the only technology partner to help with operational improvements."

- John Adams
Program Manager of Channel Enablement at Keysight