Go types for the data that business documents are made of: money amounts and
currencies, dates, IBANs and BICs, VAT IDs, country and language codes, email
addresses and messages, and UUIDs. Each type parses the many ways people and
other systems write a value, normalizes it to one canonical form, validates
it, and round-trips through database/sql, encoding/json and
encoding/xml. Optional
values have Nullable variants that map to SQL NULL and JSON null.
go get github.com/domonda/go-types
Requires Go 1.26 or later.
package main
import (
"fmt"
"github.com/domonda/go-types/bank"
"github.com/domonda/go-types/date"
"github.com/domonda/go-types/language"
"github.com/domonda/go-types/money"
"github.com/domonda/go-types/vat"
)
func main() {
// Normalize identifiers as they appear on invoices
vatID, err := vat.ID("atu 1022 3006").Normalized()
fmt.Println(vatID, err)
iban, err := bank.IBAN("DE89 3704 0044 0532 0130 00").Normalized()
fmt.Println(iban, err)
// Dots are day first, slashes month first unless a language hint says otherwise
d, err := date.Normalize("05.06.2024")
fmt.Println(d, err)
d, err = date.Normalize("05/06/2024", language.EN)
fmt.Println(d, err)
// Exact money amounts with locale-aware parsing
amount, err := money.ParseDecimalAmount("1.234,50")
fmt.Println(amount, err)
currency, err := money.Currency("€").Normalized()
fmt.Println(currency, err)
// Integer cents split without losing or inventing a cent
fmt.Println(money.CentAmount(1000).SplitEqually(3))
// Output:
// ATU10223006 <nil>
// DE89370400440532013000 <nil>
// 2024-06-05 <nil>
// 2024-05-06 <nil>
// 1234.50 <nil>
// EUR <nil>
// [3.33 3.33 3.34]
}New here? The tutorial builds an invoice record from raw strings to validated JSON and SQL values step by step.
| Package | What it gives you |
|---|---|
types (root) |
Set[T], the Validator/Validatable interfaces with Validate and DeepValidate, Normalizable, LenString, KeyMutex, Finder, pointer and iterator helpers |
account |
Alphanumeric account numbers with SQL, JSON and XML support |
bank |
IBAN and BIC validation and normalization, finders for document text, CAMT.053 XML structs |
charset |
BOM detection, UTF-16 and UTF-32, legacy charsets by IANA or MIME label |
country |
ISO 3166-1 alpha-2 country codes, alternative spellings, EU membership |
date |
ISO 8601 Date, YearMonth, YearQuarter, lenient locale-aware parsing, period ranges |
email |
Lenient address parsing, address lists and sets, MIME and TNEF message parsing and building |
float |
Locale-aware float parsing and formatting, decimal rounding, a JSON-tolerant float |
language |
ISO 639-1 language codes from BCP 47, ISO 639-2/3 and locale identifiers |
mapset |
Set algebra on map[K]struct{} and map[K]bool, mirroring the proposed container/mapset |
money |
Amount (float64), CentAmount (int64 cents), DecimalAmount (exact fixed-point), ISO 4217 currencies, rates |
notnull |
Never-null SQL/JSON wrappers: nil slices become '{}' and [] |
nullable |
Nullable wrappers that map the zero value to SQL NULL and JSON null, generic Type[T] |
queue |
Unbounded goroutine-safe FIFO queue consumed through a channel |
set |
Helpers for map[T]struct{} sets (prefer mapset for new code) |
strfmt |
Reflection-driven scanning and formatting of values with locale presets |
strutil |
Trimming, rune filtering, transliteration, file names, domain extraction, StringSet |
strutil/stringfrom |
Render pointers and optional values as strings with a text for nil |
uu |
UUID ID, NullableID, IDSlice, IDSet, v7 by default, context-injectable generation |
vat |
European VAT IDs with per-country format and checksum validation |
Every package README is the complete reference for its package: all exported types and functions, how each value is written to and read from SQL and JSON, the gotchas, and runnable examples. The same API is on pkg.go.dev.
The docs index lists everything. By what you need:
- Learn the library: Tutorial: an invoice record
- Get a task done:
- Understand the design:
- Look something up: the package READMEs in the table above.
- Identifier types like
country.Code,vat.ID,bank.IBANanduu.IDhave an invalid zero value, and theirNullableXvariant uses that zero value as SQLNULLand JSONnull. Number types likemoney.Amount, where zero is a valid value, use the genericnullable.Type[T]instead. See null conventions. Valid() boolandValidate() erroraccept the input thatNormalized()accepts, socountry.Code("de").Valid()andvat.ID("atu 1022 3006").Valid()are true.ValidAndNormalized()reports whether a value is already in canonical form. See validation and normalization.- Reading and writing trim, and uppercase IBAN, BIC, VAT, currency and
year-quarter. Country codes, language codes, email addresses and
year-months are only trimmed.
Value()writes an invalid value back.date.Dateon SQL still fully normalizes.fmtprints the stored value, see Reading and writing values. How each type is written to and read from SQL, JSON and XML, including its zero and null values, is in the Serialization section of its package README. types.Validateandtypes.DeepValidatework with any type that implementsValidate() errororValid() bool, including your own.
SQL, JSON, XML and text only prettify a value. They trim surrounding
whitespace, and uppercase letters where the type is written in upper case:
bank.IBAN, bank.BIC, vat.ID, money.Currency, date.YearQuarter.
country.Code, language.Code, email.Address and date.YearMonth are
only trimmed, so "Deutschland" stays "Deutschland" and "EN" stays
"EN". An invalid value is written back after that prettification.
Value does not refuse it. Normalized() is unchanged and is what
ScanString(s, true) stores. Printing shows what is stored.
date.Date is the exception on SQL. Scan and Value fully normalize,
because a database date is not free text. A time.Time from the driver is
stored with OfTime, which is already YYYY-MM-DD. JSON and XML only trim.
| Format | Read | Write |
|---|---|---|
SQL, database/sql |
Scan |
Value |
JSON, encoding/json |
UnmarshalJSON |
MarshalJSON |
XML, encoding/xml |
UnmarshalXML, UnmarshalXMLAttr |
MarshalText, MarshalXMLAttr |
Text, strfmt and CSV |
ScanString |
MarshalText |
| JSON object keys of a map | as given | as given |
fmt, templates, logs |
as stored |
| Input | Scan, JSON, XML, ScanString(s, false) |
ScanString(s, true) |
|---|---|---|
"DE" |
"DE" |
"DE" |
" de " |
"de" |
"DE" |
"Deutschland" |
"Deutschland" |
"DE" |
"XX" |
"XX", no error |
error |
"", " " |
"" |
Code: error, NullableCode: null |
SQL NULL |
"" |
|
JSON null |
Code: unchanged, NullableCode: "" |
|
| JSON number, SQL integer | error |
For date.Date and date.NullableDate, SQL Scan and Value treat the
zero dates 0001-01-01 and 0000-00-00, and spellings of them like
01.01.0001, as blank. JSON and XML only trim, so those spellings stay.
A time.Time from the driver is stored with OfTime, and the zero
time.Time as "". date.YearMonth and date.YearQuarter read a
time.Time as its month or quarter the same way, and keep a zero value
such as "0001-01" or "0001-Q1" when it is already text.
Reading never fails because of the text itself. An invalid value is kept
after prettification, so one bad row doesn't fail a whole query or JSON
document, and types.DeepValidate reports it together with its field path.
| Value in Go | Value (SQL) |
MarshalJSON |
MarshalText, XML |
fmt, String |
|---|---|---|---|---|
"DE" |
DE |
"DE" |
DE |
DE |
"de", converted in Go |
de |
"de" |
de |
de |
"XX" |
XX |
"XX" |
XX |
XX |
"" |
NULL |
Code: "", NullableCode: null |
empty | empty |
fmt, templates and String show the value as it is stored. Normalized()
is the canonical spelling, and it is not applied while reading or writing.
XML writes <country>DE</country> for an element and country="DE" for an
attribute field. A nil *country.Code attribute field is left out, with or
without omitempty. A nil pointer ,chardata field still makes
encoding/xml panic, like a nil *time.Time, because encoding/xml calls
MarshalText through the nil pointer and has no hook to skip it; use a
non-pointer ,chardata field.
| Types | Difference |
|---|---|
country.Code, money.Currency, language.Code |
String() is generated by go-enum and returns the stored value |
date.YearMonth, date.YearQuarter and their nullable types |
String() returns the stored value |
vat.ID, bank.IBAN, bank.BIC, date.Date, email.Address |
no String() method, fmt prints the stored value |
NullableX variants |
blank is null; StringOr(alt) returns alt only for null; ScanString also reads NULL, null and nil as null |
language.Code |
no nullable variant, its "" is null |
email.AddressList, email.NullableAddressList |
the list string is trimmed; addresses are not parsed or lowercased; Value does not reject an invalid list; normalize with Normalized() |
email.AddressSet, types.Set[T] |
keys as stored, so {"a@x.com", "A@x.com"} and a padded key stay distinct; AddressSet writes a PostgreSQL text[] array in SQL, a sorted array as JSON, and an address list as text and XML; MarshalText refuses a key that would not read back as one list entry; normalize with Normalized() or NormalizedAddressList() |
Scan and UnmarshalJSON of the email collections |
read a PostgreSQL array, a JSON array, a JSON object with the addresses as keys, a JSON string and a plain address list |
money.CurrencyAmount, money.CurrencyDecimalAmount |
currency trimmed and uppercased in Value and JSON, as stored in String |
notnull.TrimmedString, nullable.TrimmedString |
trimmed when read and in String |
notnull.TrimmedString, nullable.TrimmedString, account.Number, account.NullableNumber |
encoding/json reads the keys of a map of them trimmed, so " a" and "a" merge into one key |
A set writes its elements as given because normalizing could merge two of
them, and the set would come back with fewer elements than it was written
with. For the same reason the keys of a map[country.Code]V are read and
written as given in JSON. The normalizing types have no UnmarshalText for
that reason: encoding/json decodes a map key of a type with UnmarshalText
through it, and in Go 1.26 through its UnmarshalJSON. The trimming string
types and account.Number keep UnmarshalText, because trimming is what
they are for and flag.TextVar and other text decoders need it, so their
map keys are trimmed.
These rules decide how every type behaves, and how an inconsistency between types gets resolved. Types that don't follow a rule yet are tracked in TODOS.md.
- Be generous in accepting input, and do not rewrite stored data.
Normalized()andValidate()accept every common spelling:country.Code("de"),vat.ID("atu 1022 3006")andmoney.Currency("€")are valid input. Reading and writing do not apply that canonical form. They only trim, and uppercase where the type is written in upper case, so a value already in the database is not replaced by a stricter spelling.date.Dateon SQL is the exception, anduu.IDstays fully canonical. CallNormalized()when a comparison or a map key needs the canonical form. The email types have an extraASCII()for message headers. - Show what is stored.
fmt, templates andStringprint a value as it is stored, never normalized, so a log can't show a non-canonical or invalid value as if it were the canonical one. - Collections keep their elements as given.
types.Setandemail.AddressSetread and write their keys as stored, because normalizing could merge two elements, and a set would read back with fewer elements than it was written with. An address list trims the list string and does not parse or lowercase the addresses.Valuedoes not reject an invalid list. Normalizing them is an explicit call. - Empty input is null. For a nullable type, an empty or whitespace-only
string from SQL, JSON or text is null, because an empty CSV cell or text
column means "no value". A non-nullable type reads it as its empty value,
which is invalid, and writes that as SQL
NULL, so aNOT NULLcolumn rejects it. - Fail loudly instead of guessing. Input that contradicts itself, or whose
meaning depends on something the package can't know, is an error instead of
a silently wrong value. A number with an ordinal suffix is never read as the
month, so
"13 1st 2024"is an error, andstrfmtrejects a time zone abbreviation whose offset is unknown on the machine instead of reading the time as UTC. A documented default is no guess: an ambiguous date like05.06.2024is day first with dots, and05/06/2024is month first with slashes unless a language hint says otherwise. - Errors are created with
github.com/domonda/go-errs(errs.New,errs.Errorf) instead oferrors.Newandfmt.Errorf.
go test ./... # all tests
./run-go-enum.sh # regenerate Enums()/EnumStrings() after adding a code
./run-gosec.sh # static security analysis
go-enum and gosec are pinned as tool dependencies in tools/go.mod, so the
main module does not depend on them. CI builds, vets, checks that the
generated enum methods are up to date and runs the tests with the race
detector on every pull request.
No version has been tagged yet. CHANGELOG.md lists every
change, including the breaking ones, and TODOS.md lists what is
left before v1.0.0.
MIT, see LICENSE. The uu package is derived from
github.com/satori/go.uuid.