Marshal and unmarshal Go structs to and from fixed-width line formats using struct tags.
Built for Scandinavian payment file formats (Nets AvtaleGiro, OCR Giro, Bankgirot AutoGiro) but works with any 80-character (or custom width) fixed-position record format.
go get github.com/karolusz/ocrline
Define structs with ocr tags specifying 0-based byte positions (Go slice convention):
type TransmissionStart struct {
FormatCode string `ocr:"0:2"`
ServiceCode string `ocr:"2:4"`
TransactionType string `ocr:"4:6"`
RecordType int `ocr:"6:8"`
DataTransmitter string `ocr:"8:16"`
TransmissionNo string `ocr:"16:23"`
DataRecipient string `ocr:"23:31"`
}line := "NY000010555555551000081000080800000000000000000000000000000000000000000000000000"
var record TransmissionStart
if err := ocrline.Unmarshal(line, &record); err != nil {
log.Fatal(err)
}
fmt.Println(record.FormatCode) // "NY"
fmt.Println(record.DataTransmitter) // "55555555"
fmt.Println(record.RecordType) // 10record := TransmissionStart{
FormatCode: "NY",
ServiceCode: "00",
TransactionType: "00",
RecordType: 10,
DataTransmitter: "55555555",
TransmissionNo: "1000081",
DataRecipient: "00008080",
}
line, err := ocrline.Marshal(record)
// "NY000010555555551000081000080800000000000000000000000000000000000000000000000000"ocr:"start:end"
ocr:"start:end,option,option,..."
Positions are 0-based, exclusive end (like Go slices). ocr:"0:2" reads line[0:2].
| Option | Description |
|---|---|
align-left |
Left-align the value in the field |
align-right |
Right-align the value in the field |
pad-zero |
Pad with '0' characters |
pad-space |
Pad with ' ' characters |
omitempty |
If zero-valued, fill with padding instead of the value |
| Go Type | Default Alignment | Default Padding |
|---|---|---|
string |
left | space |
int, int8..int64 |
right | zero |
uint, uint8..uint64 |
right | zero |
bool |
right | zero |
*T |
inherits from T |
inherits from T |
Named types follow their underlying type: type Numeric string gets string defaults.
Embedded structs are flattened, just like encoding/json:
type RecordBase struct {
FormatCode string `ocr:"0:2"`
ServiceCode string `ocr:"2:4"`
RecordType int `ocr:"6:8"`
}
type PaymentRecord struct {
RecordBase
PayerNumber string `ocr:"15:31"`
Amount int `ocr:"31:43"`
}Embedded pointer structs (*RecordBase) are also supported. On unmarshal, nil pointers are auto-allocated. On marshal, nil embedded pointers are skipped (gaps filled with default).
Byte positions not covered by any field are filled with '0' by default. To fill specific gaps with a different character, implement the Filler interface:
func (r PaymentRecord) OCRFill() []ocrline.Fill {
return []ocrline.Fill{
{Start: 8, End: 15, Char: ' '}, // positions 8-14 filled with spaces
}
}Implement Marshaler and Unmarshaler for full control over field serialization:
type ServiceCode string
func (s ServiceCode) MarshalOCR() (string, error) {
return string(s), nil
}
func (s *ServiceCode) UnmarshalOCR(data string) error {
*s = ServiceCode(strings.TrimSpace(data))
return nil
}When parsing files with multiple record types, unmarshal a small header struct first to determine the type, then unmarshal the full line into the correct struct:
var header struct {
FormatCode string `ocr:"0:2"`
RecordType int `ocr:"6:8"`
}
ocrline.Unmarshal(line, &header)
switch header.RecordType {
case 10:
var r TransmissionStart
ocrline.Unmarshal(line, &r)
case 89:
var r TransmissionEnd
ocrline.Unmarshal(line, &r)
}Both calls are cheap - struct metadata is cached per type, and lines are only 80 characters.
Marshal outputs 80 characters by default. Use MarshalWidth for other widths:
line, err := ocrline.MarshalWidth(record, 120) // 120-char line
line, err := ocrline.MarshalWidth(record, 0) // no padding, exact width of rightmost fieldStruct metadata is parsed, validated, and cached per type on first use (same pattern as encoding/json). Subsequent calls for the same struct type have zero reflection overhead for tag parsing.
Validated once per type:
- Tag syntax - start and end must be integers,
start >= 0,end > start - Overlapping fields - two fields covering the same positions are rejected with
*OverlapError - Invalid tags - malformed
ocrtags produce*TagError
Validated per call:
- Out-of-range fields - on unmarshal, fields exceeding the line length produce
*UnmarshalRangeError - Unexported fields are silently skipped (same as
encoding/json)
func Marshal(v any) (string, error)
func MarshalWidth(v any, width int) (string, error)
func Unmarshal(line string, v any) errortype Marshaler interface {
MarshalOCR() (string, error)
}
type Unmarshaler interface {
UnmarshalOCR(data string) error
}
type Filler interface {
OCRFill() []Fill
}The library is format-agnostic. It has been designed with these formats in mind:
| Format | Country | Provider | Line Width |
|---|---|---|---|
| AvtaleGiro | Norway | Nets | 80 |
| OCR Giro | Norway | Nets | 80 |
| AutoGiro | Sweden | Bankgirot | 80 |
MIT