fiscal is a Go library for parsing and marshaling Brazilian fiscal XML
documents with strongly typed structs generated from the adapted
official XSD schemas.
It currently covers:
- NF-e
- NFS-e
- CT-e
- MDF-e
- BP-e
The library supports primary documents, processed wrappers, distribution responses, service/status and consultation roots where implemented, and typed event documents where applicable.
git clone git@github.com:awafinance/fiscal.git
cd fiscal
mise trust && mise installThe root package can parse any supported fiscal XML by detecting the root namespace:
import "github.com/awafinance/fiscal"It exposes the main entrypoint:
Parse(data []byte) (*Document, error)
ParseReader(r io.Reader) (*Document, error)The returned fiscal.Document is a tagged container with Family,
RootName, and one populated family document field:
doc, err := fiscal.Parse(data)
if err != nil {
panic(err)
}
switch doc.Family {
case fiscal.NFe:
fmt.Println(doc.NFe.GetAccessKey())
case fiscal.MDFe:
fmt.Println(doc.MDFe.GetRelatedDocuments())
}Each document family is also exposed through its own package when callers already know the family or need the family-specific typed API:
import "github.com/awafinance/fiscal/pkg/nfe"
import "github.com/awafinance/fiscal/pkg/nfse"
import "github.com/awafinance/fiscal/pkg/cte"
import "github.com/awafinance/fiscal/pkg/mdfe"
import "github.com/awafinance/fiscal/pkg/bpe"Each package exposes the same core entrypoint:
Parse(data []byte) (*Document, error)
ParseReader(r io.Reader) (*Document, error)The returned Document is a tagged container where exactly one root field is
expected to be populated.
The generated XML schema types are also part of the public API through each
family package, so external consumers can build and pass typed values without
importing internal/....
var inf *nfe.NFeProcTNFe
doc := &nfe.Document{NFe: inf}package main
import (
"fmt"
"os"
"github.com/awafinance/fiscal"
)
func main() {
data, err := os.ReadFile("document.xml")
if err != nil {
panic(err)
}
doc, err := fiscal.Parse(data)
if err != nil {
panic(err)
}
info := doc.Info()
fmt.Println(info.GetAccessKey())
fmt.Println(info.GetIssuer())
fmt.Println(info.GetAmount())
}fiscal.Parse first reads the XML root element namespace, routes the document
to the matching family package, and then unmarshals into the corresponding
schema-generated type.
For families that use generic event envelopes, parsing also dispatches by tpEvento, so event documents can be represented with concrete event-specific structs instead of a single generic event shape.
Examples of supported dispatch patterns:
- NF-e: invoice, processed invoice, consultation/status/inutilizacao roots, distribution, concrete events
- CT-e: document variants, processed wrappers, modal/event variants, distribution
- MDF-e: consultation roots, processed roots, distribution roots, concrete events
- BP-e: base documents and concrete event variants
All supported family documents implement a common DocumentInfo interface.
The root fiscal.Document.Info() method returns that interface:
type DocumentInfo interface {
GetAccessKey() string
GetVersion() string
GetEnvironment() string
GetNumber() string
GetSeries() string
GetModel() string
GetIssueDate() string
GetAmount() string
GetIssuer() string
GetIssuerDocument() string
GetRecipient() string
GetRecipientDocument() string
GetProtocolNumber() string
GetStatusCode() string
GetStatusReason() string
IsAuthorized() bool
}These methods return the XML values as strings. The library does not parse, round, sum, or normalize monetary values.
doc, err := fiscal.Parse(data)
if err != nil {
panic(err)
}
info := doc.Info()
fmt.Println(info.GetAccessKey())
fmt.Println(info.GetVersion())
fmt.Println(info.GetEnvironment())
fmt.Println(info.GetAmount())
fmt.Println(info.GetIssuerDocument())Some fiscal concepts are not universal. For those, the library exposes small optional interfaces. Consumers can type-assert only the detail they need:
if amounts, ok := doc.Info().(fiscal.AmountsInfo); ok {
for _, amount := range amounts.GetAmounts() {
fmt.Println(amount.Type, amount.Value)
}
}
if parties, ok := doc.Info().(fiscal.PartiesInfo); ok {
for _, party := range parties.GetParties() {
fmt.Println(party.Role, party.Name, party.Document)
}
}
if related, ok := doc.Info().(fiscal.RelatedDocumentsInfo); ok {
for _, ref := range related.GetRelatedDocuments() {
fmt.Println(ref.Type, ref.AccessKey)
}
}
if route, ok := doc.Info().(fiscal.RouteInfo); ok {
fmt.Println(route.GetModal())
fmt.Println(route.GetOrigin())
fmt.Println(route.GetDestination())
}Optional interface support is intentionally grouped by concept:
AmountsInforeturns raw amount fields such as NFe total, CTe service value, MDFe cargo value, BPe ticket value, and NFSe service/net values.PartiesInforeturns known parties with roles, such as issuer, recipient, provider, taker, sender, dispatcher, receiver, and buyer.RelatedDocumentsInforeturns document references such as linked NFe, CTe, MDF-e, or DCe access keys where the schema carries them.RouteInforeturns modal, origin, and destination fields for transport and service documents where those concepts exist.
The pkg/info package contains the shared structs and optional interface
definitions. The root package re-exports them as aliases, so callers can use
fiscal.Amount, fiscal.Party, fiscal.RelatedDocument, and
fiscal.Location.
NFe also exposes convenience methods for line items and payments:
for _, item := range doc.NFe.GetItems() {
fmt.Println(item.Number, item.Code, item.Description, item.Amount)
}
for _, payment := range doc.NFe.GetPayments() {
fmt.Println(payment.Method, payment.Amount)
}These methods also return the raw XML string values.
Parsed documents can be marshaled back through the standard library encoder:
out, err := xml.MarshalIndent(doc, "", " ")The library implements custom MarshalXML logic so the original supported root is preserved.
JSON output also includes rootName so parsed documents can preserve their original root selection when marshaled back to XML after a JSON round-trip.
- A
Documentmust contain exactly one supported root field. Setting multiple root fields is invalid. - XML round-tripping preserves the parsed root only when
rootNameis available. Parsed documents populate it automatically, and JSON output now carries it as well. - NF-e marshaling emits
nfeProcwhen protocol data is present. A document withProtNFeis not re-encoded as bareNFe. - The supported typed API is the alias surface exported from
pkg/<family>/types.go. Depending directly oninternal/...generated packages is not supported.
The repository also includes a small CLI for parsing fiscal XML documents. The family is detected automatically from the root namespace.
Run it from the repository root with:
go run ./cmd --help
go run ./cmd <xml> # human-readable summary
go run ./cmd <xml> --json # full typed document as JSONThe default output prints a summary built from the shared accessors (access key, version, number/series, issuer, recipient, amount, status, etc.) plus any extra sections the family exposes (amounts, parties, route, related documents).
For example:
go run ./cmd testdata/nfe/35180834128745000152550010000476121675985748-nfe.xml
go run ./cmd --json testdata/cte/v4_0/43120178408960000182570010000000041000000047-cte.xml | jq '.cte'pkg/<family>contains the public parsing APIinternal/<family>/schemascontains normalized XSDs used for generationinternal/<family>/gencontains generated Go bindingsinternal/<family>/tools/codegencontains normalization and post-processing helpers
Generation is schema-driven. The generated code is post-processed to fix XML namespace handling, normalize anonymous types, and keep marshal/unmarshal behavior stable across families.
Regenerate bindings for a family with:
mise run genRun tests with:
mise run testCheck all commands available:
mise tasksThis project was inspired by nfelib.
Thanks to the nfelib maintainers for the reference implementation and
for the test files that helped shape and validate this library.