Writing

Serialize segments back to the EDIFACT wire format with Writer, automatic delimiter escaping, custom UNA service strings, and repeating data elements.

On this page
  1. Overview of write APIs
  2. Serialize a typed struct
  3. Round-trip: parse then write
  4. Streaming writer
    1. Segment count tracking
  5. write_elements — mixed simple and composite elements
  6. write_raw — runtime string data
  7. Custom delimiters and UNA
  8. Repeating data elements
    1. Through the event layer
  9. Writing in a non-UTF-8 repertoire
  10. Character repertoires
  11. Escape handling
  12. Event-based writing (WriterEmitter)
  13. Writing a full interchange
  14. Next steps

This guide covers every way to produce EDIFACT output — from typed structs to raw segment construction and custom delimiter configuration.


Overview of write APIs

APIBest for
to_edifact_string(value)Quick serialization of a single derived struct
ser::to_bytes(segments)Round-trip a parsed Vec<Segment<'_>>
to_bytes(segments)Free function alias for ser::to_bytes
Writer::write_segment(seg)Streaming segment-by-segment output
Writer::write_elements(tag, elements)The general form — segments mixing simple and composite data elements
Writer::write_composites(tag, elements)Segments whose every element is a composite
Writer::write_raw(tag, elements)All-simple segments, component boundaries inferred by splitting
Writer::write_segment_parts(tag, elements)Same as write_elements, for owned String data
Writer::with_una(w, ssa)Output with custom UNA service string

Serialize a typed struct

use edifact_rs::{EdifactSerialize, ser, to_edifact_string};

#[derive(edifact_rs::EdifactSerialize)]
#[edifact(segment = "BGM")]
struct Bgm {
    #[edifact(element = 0)]
    doc_code: String,
    #[edifact(element = 1)]
    doc_number: String,
    #[edifact(element = 2)]
    function_code: Option<String>,
}

let bgm = Bgm {
    doc_code: "220".into(),
    doc_number: "PO-4711".into(),
    function_code: Some("9".into()),
};

let output = to_edifact_string(&bgm)?;
assert_eq!(output, "BGM+220+PO-4711+9'");

// or as bytes:
let bytes = ser::to_bytes(&bgm)?;
# Ok::<(), edifact_rs::EdifactError>(())

None fields produce empty elements in their positional slot:

# use edifact_rs::to_edifact_string;
# #[derive(edifact_rs::EdifactSerialize)]
# #[edifact(segment = "BGM")]
# struct Bgm {
#     #[edifact(element = 0)]
#     doc_code: String,
#     #[edifact(element = 1)]
#     doc_number: String,
#     #[edifact(element = 2)]
#     function_code: Option<String>,
# }
# 
# let bgm = Bgm {
#     doc_code: "220".into(),
#     doc_number: "PO-4711".into(),
#     function_code: Some("9".into()),
# };
let bgm_no_func = Bgm {
    doc_code: "220".into(),
    doc_number: "PO-4711".into(),
    function_code: None,
};
let out = to_edifact_string(&bgm_no_func)?;
assert_eq!(out, "BGM+220+PO-4711+'");
# Ok::<(), edifact_rs::EdifactError>(())

Round-trip: parse then write

use edifact_rs::{from_bytes, segments_to_bytes};

let input = b"UNA:+.? 'BGM+220+PO-4711+9'NAD+BY+4000001::9'";
let segs: Vec<_> = from_bytes(input).collect::<Result<_, _>>()?;

let output = segments_to_bytes(&segs)?;
assert_eq!(output, b"BGM+220+PO-4711+9'NAD+BY+4000001::9'");
# Ok::<(), edifact_rs::EdifactError>(())

segments_to_bytes is the entry point for a slice of parsed Segments; ser::to_bytes is for a value that implements EdifactSerialize.

Note: segments_to_bytes writes with the default EDIFACT delimiters and emits no UNA header, so the round-trip above is not byte-for-byte when the input carried one. Use Writer::with_una to preserve a custom service string.


Streaming writer

Writer<W> writes one segment at a time to any Write implementation:

use edifact_rs::{Writer, Segment, Element};

let mut buf: Vec<u8> = Vec::new();
let mut writer = Writer::new(&mut buf);

writer.write_segment(&Segment::new("BGM", vec![
    Element::of(&["220"]),
    Element::of(&["PO-4711"]),
    Element::of(&["9"]),
]))?;

writer.write_segment(&Segment::new("NAD", vec![
    Element::of(&["BY"]),
    Element::of(&["4000001", "", "9"]), // composite element
]))?;

writer.finish()?;

let text = String::from_utf8(buf).unwrap();
assert_eq!(text, "BGM+220+PO-4711+9'NAD+BY+4000001::9'");
# Ok::<(), edifact_rs::EdifactError>(())

Writer::finish() flushes the underlying writer and returns it.

Segment count tracking

Writer maintains an internal segment counter that is incremented on every write_segment call. Retrieve it with writer.segment_count() to fill the UNT segment's count field:

# use edifact_rs::{Writer, Segment, Element};
# let mut buf: Vec<u8> = Vec::new();
# let mut writer = Writer::new(&mut buf);
// ... write body segments ...

let count = writer.segment_count() + 2; // +2 for UNH and UNT themselves
writer.write_raw("UNT", &[&count.to_string(), "1"])?;
# Ok::<(), edifact_rs::EdifactError>(())

write_elements — mixed simple and composite elements

Most real EDIFACT segments mix the two shapes: NAD takes a simple qualifier followed by a composite party identification, DTM takes a single composite. write_elements expresses that directly, with component boundaries given explicitly rather than inferred:

use edifact_rs::{DataElement, Writer};

let mut buf: Vec<u8> = Vec::new();
let mut writer = Writer::new(&mut buf);

writer.write_elements("NAD", &[
    DataElement::Simple("MS"),
    DataElement::Composite(&["9900112233445", "", "293"]),
])?;
writer.write_elements("DTM", &[
    DataElement::Composite(&["137", "20260101", "102"]),
])?;

writer.finish()?;
assert_eq!(
    String::from_utf8(buf).unwrap(),
    "NAD+MS+9900112233445::293'DTM+137:20260101:102'",
);
# Ok::<(), edifact_rs::EdifactError>(())

The elements! macro is shorthand for the same thing. Each entry is an ordinary expression borrowed through the AsDataElement trait: a string becomes a simple data element, an array/slice/Vec of strings becomes a composite. Runtime values work exactly like literals, which is the point — builders rarely have literals:

use edifact_rs::{Writer, elements};

let qualifier = String::from("MS");
let gln = "9900112233445";

let mut buf: Vec<u8> = Vec::new();
let mut writer = Writer::new(&mut buf);

writer.write_elements("NAD", elements![qualifier.as_str(), [gln, "", "293"]])?;
writer.write_elements("DTM", elements![["137", "20260101", "102"]])?;

writer.finish()?;
assert_eq!(
    String::from_utf8(buf).unwrap(),
    "NAD+MS+9900112233445::293'DTM+137:20260101:102'",
);
# Ok::<(), edifact_rs::EdifactError>(())

Composite components must be string slices: a [String; N] cannot borrow as &[&str] without allocating, so write [id.as_str(), "", agency].

Because boundaries are explicit, a value containing a literal component separator is escaped rather than silently promoted to a boundary — which is the failure mode of pre-joining components into one string:

# use edifact_rs::{Writer, elements};
# let mut buf: Vec<u8> = Vec::new();
# let mut writer = Writer::new(&mut buf);
writer.write_elements("NAD", elements!["MS", "ACME:INC"])?;
writer.finish()?;
// The `:` stays inside the value:
assert_eq!(String::from_utf8(buf).unwrap(), "NAD+MS+ACME?:INC'");
# Ok::<(), edifact_rs::EdifactError>(())

MessageWriter — the UNH/UNT guard from Writer::begin_message — carries the same methods (write_elements, write_composites, write_segment_parts, write_raw, write_segment), so every segment you write inside a message is counted into the UNT DE 0074 total.


write_raw — runtime string data

When building segments from runtime data (e.g., database values), use write_raw to avoid constructing Segment / Element objects:

use edifact_rs::Writer;

let mut buf: Vec<u8> = Vec::new();
let mut writer = Writer::new(&mut buf);

// write_raw(tag, &[elements]) — components inside an element separated by ':'
writer.write_raw("DTM", &["137:20240101:102"])?;
writer.write_raw("RFF", &["ON:PO-4711"])?;
// Caveat: the split is on the *active* component separator, and a literal `:`
// inside a value becomes a boundary. Prefer `write_elements` when either
// matters.

writer.finish()?;
# Ok::<(), edifact_rs::EdifactError>(())

Custom delimiters and UNA

To write with non-default delimiters, create the writer with Writer::with_una:

use edifact_rs::{Writer, ServiceStringAdvice};

let ssa = ServiceStringAdvice {
    component_sep: b';',
    element_sep:   b'|',
    decimal_mark:  b'.',
    release_char:  b'?',
    repetition_sep: b' ',   // 0x20 = 'not used'
    segment_term:  b'!',
};

let mut buf: Vec<u8> = Vec::new();
let mut writer = Writer::with_una(&mut buf, ssa)?;

// One `&str` per data element; `write_raw` splits each on the *component*
// separator, which this UNA sets to `;`.
writer.write_raw("BGM", &["220", "PO-4711", "9"])?;
writer.finish()?;

let text = String::from_utf8(buf).unwrap();
// UNA header first (9 bytes, including the space "not used" repetition slot),
// then the segment with the custom delimiters.
assert_eq!(text, "UNA;|.? !BGM|220|PO-4711|9!");
# Ok::<(), edifact_rs::EdifactError>(())

Repeating data elements

ISO 9735-1 §8.6 repetitions are written with the repetition separator from UNA position 050, so the writer needs a ServiceStringAdvice that declares one:

use edifact_rs::{Element, Segment, ServiceStringAdvice, Writer};

let ssa = ServiceStringAdvice { repetition_sep: b'*', ..Default::default() };
let mut buf: Vec<u8> = Vec::new();
{
    let mut writer = Writer::with_una(&mut buf, ssa)?;
    let rff = Segment::new(
        "RFF",
        vec![Element::of(&["ON", "1"]).and_repeat(&["ON", "2"])],
    );
    writer.write_segment(&rff)?;
}
assert!(String::from_utf8(buf).unwrap().ends_with("RFF+ON:1*ON:2'"));
# Ok::<(), edifact_rs::EdifactError>(())

Writing a repeating element through a writer that has no declared separator is refused with EdifactError::RepetitionSeparatorNotDeclared (E037) rather than silently joined with the space sentinel — which would read back as a single occurrence.

The refusal is checked before the first byte leaves the writer, so a rejected segment writes nothing. The writer stays usable: log the error, skip the segment, and keep going — the next segment starts at a clean boundary rather than after a dangling RFF+.

Through the event layer

EdifactEvent::RepeatElement is the write-side mirror of the parser's repetition split, so a value that arrives as several occurrences can leave as several occurrences:

use edifact_rs::{EdifactEvent, EventEmitter, ServiceStringAdvice, WriterEmitter};

let ssa = ServiceStringAdvice::from_bytes(b"UNA:+.?*'")?;
let mut emitter = WriterEmitter::with_una(Vec::new(), ssa)?;
emitter.emit(EdifactEvent::StartSegment { tag: "RFF" })?;
emitter.emit(EdifactEvent::Element { value: "ON" })?;
emitter.emit(EdifactEvent::ComponentElement { value: "1" })?;
emitter.emit(EdifactEvent::RepeatElement { value: "ON" })?;  // second occurrence
emitter.emit(EdifactEvent::ComponentElement { value: "2" })?;
emitter.emit(EdifactEvent::EndSegment)?;

assert_eq!(emitter.finish()?, b"UNA:+.?*'RFF+ON:1*ON:2'".to_vec());
# Ok::<(), edifact_rs::EdifactError>(())

The same E037 refusal applies: a RepeatElement under a writer with no declared separator is rejected before the separator byte is written.


Writing in a non-UTF-8 repertoire

Writer::with_charset binds the writer to the repertoire named in UNB S001, so a UNOC interchange goes out as ISO 8859-1 rather than as UTF-8 — and a character the repertoire cannot carry is refused instead of being written as bytes the receiver decodes as something else.

The typed path reaches it through WriterEmitter::with_charset, so a #[derive(EdifactSerialize)] struct is bound the same way:

use edifact_rs::{Charset, EdifactEvent, EventEmitter, WriterEmitter};

let mut emitter = WriterEmitter::new(Vec::new()).with_charset(Charset::UnoC);
emitter.emit(EdifactEvent::StartSegment { tag: "NAD" })?;
emitter.emit(EdifactEvent::Element { value: "Müller" })?;
emitter.emit(EdifactEvent::EndSegment)?;

// `ü` goes out as the single Latin-1 byte 0xFC, not as two UTF-8 bytes.
assert_eq!(emitter.finish()?, b"NAD+M\xFCller'".to_vec());
# Ok::<(), edifact_rs::EdifactError>(())

See Character Sets for the full repertoire table.


Character repertoires

UNB S001 names the repertoire the payload is written in. A writer that emits UTF-8 into a UNOC interchange produces mojibake at the far end with nothing in the file to explain it, so bind the writer to the repertoire and let it encode:

use edifact_rs::{Charset, Writer};

let mut writer = Writer::new(Vec::new()).with_charset(Charset::UnoC);
writer.write_composites("NAD", &[&["BY"], &["Müller"]])?;
// `ü` goes out as the single ISO 8859-1 byte 0xFC.
assert_eq!(writer.finish()?, b"NAD+BY+M\xFCller'".to_vec());
# Ok::<(), edifact_rs::EdifactError>(())

A character the repertoire cannot carry is refused with EdifactError::CharacterNotInRepertoire (E038), and a UNB declaring a repertoire the writer does not encode is refused with CharacterRepertoireMismatch (E041) — the header cannot lie about the body.

See Character Sets for the read side and for repertoire validation.


Escape handling

Writer automatically escapes any character in a component value that collides with the current delimiter set. You never need to pre-escape data:

use edifact_rs::{Writer, Segment, Element};

let mut buf: Vec<u8> = Vec::new();
let mut writer = Writer::new(&mut buf);

// Both `:` and `+` are delimiters — the writer escapes each of them.
writer.write_segment(&Segment::new("FTX", vec![
    Element::of(&["AAI"]),
    Element::of(&["Price: 100+VAT"]),  // ':' → '?:'  and  '+' → '?+'
]))?;
writer.finish()?;

let text = String::from_utf8(buf).unwrap();
assert_eq!(text, "FTX+AAI+Price?: 100?+VAT'");
# Ok::<(), edifact_rs::EdifactError>(())

Characters escaped by default:

  • ' (segment terminator)
  • + (element separator)
  • : (component separator)
  • ? (the release character itself)

Plus the repetition separator, when the active UNA declares one. The space "not used" sentinel at UNA position 7 is never escaped.


Event-based writing (WriterEmitter)

For advanced use cases — such as writing segments produced by EdifactSerialize derived types to a Write sink without buffering — use WriterEmitter:

use edifact_rs::{ser, WriterEmitter, EdifactSerialize, Writer};

# #[derive(EdifactSerialize)]
# #[edifact(segment = "BGM")]
# struct Bgm { #[edifact(element = 0)] code: String }
let bgm = Bgm { code: "220".into() };

let mut buf: Vec<u8> = Vec::new();
// `WriterEmitter::new` takes the `io::Write` sink directly and constructs its
// own `Writer` internally — do not wrap the sink in a `Writer` first.
let mut emitter = WriterEmitter::new(&mut buf);

bgm.edifact_serialize(&mut emitter)?;

// `finish` flushes and hands back the sink that was passed in.
let _sink = emitter.finish()?;
# Ok::<(), edifact_rs::EdifactError>(())

WriterEmitter is allocation-free per event: it writes directly to the underlying Write on each EdifactEvent without buffering components in a String.


Writing a full interchange

use edifact_rs::{Writer, Segment, Element};
use std::io::Cursor;

let mut buf: Vec<u8> = Vec::new();
let mut w = Writer::new(&mut buf);

// Interchange header
w.write_raw("UNB", &["UNOA:1", "SENDER:14", "RECEIVER:14", "200101:0900", "1"])?;
// Message header
w.write_raw("UNH", &["1", "ORDERS:D:96A:UN"])?;
// Body
w.write_raw("BGM", &["220", "PO-4711", "9"])?;
w.write_raw("NAD", &["BY", "4000001::9"])?;
// Message trailer (segment count includes UNH and UNT)
let body_segments = w.segment_count(); // BGM + NAD
let unt_count = body_segments + 2;
w.write_raw("UNT", &[&unt_count.to_string(), "1"])?;
// Interchange trailer
w.write_raw("UNZ", &["1", "1"])?;

w.finish()?;
# Ok::<(), edifact_rs::EdifactError>(())

Next steps

  • Typed Derive — derive EdifactSerialize for your structs
  • Parsing — parse EDIFACT input to Segment slices
  • Performance — allocation budgets and benchmarking tips