fiscal
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.
Setup
git clone git@github.com:awafinance/fiscal.git
cd fiscal
mise trust && mise install
Packages
The 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}
Basic usage
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())
}
Document dispatch
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
Convenience accessors
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:
AmountsInfo returns raw amount fields such as NFe total, CTe service value,
MDFe cargo value, BPe ticket value, and NFSe service/net values.
PartiesInfo returns known parties with roles, such as issuer, recipient,
provider, taker, sender, dispatcher, receiver, and buyer.
RelatedDocumentsInfo returns document references such as linked NFe, CTe,
MDF-e, or DCe access keys where the schema carries them.
RouteInfo returns 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 Details
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.
Round-trip behavior
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.
Limitations
- A
Document must contain exactly one supported root field. Setting multiple root fields is invalid.
- XML round-tripping preserves the parsed root only when
rootName is available. Parsed documents populate it automatically, and JSON output now carries it as well.
- NF-e marshaling emits
nfeProc when protocol data is present. A document with ProtNFe is not re-encoded as bare NFe.
- The supported typed API is the alias surface exported from
pkg/<family>/types.go. Depending directly on internal/... generated packages is not supported.
CLI
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 JSON
The 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'
Project structure
pkg/<family> contains the public parsing API
internal/<family>/schemas contains normalized XSDs used for generation
internal/<family>/gen contains generated Go bindings
internal/<family>/tools/codegen contains 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.
Development
Regenerate bindings for a family with:
mise run gen
Run tests with:
mise run test
Check all commands available:
mise tasks
Acknowledgements
This 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.
License
Apache-2.0