6.1 KiB
fakes
A Go library for generating fake data, built for internationalization. It exists because the existing Go fake libraries lacked the locale coverage and format control we needed.
- Standards first — formats, names and structures follow international and local standards first and foremost.
- Locale-aware — names, addresses, postal codes and phone numbers follow
per-locale data and formats. Locales are always full language + territory
tags (
sv_SE, neversv). - Data lives in JSON — all source data is recursive JSON on disk, read when you create a faker and then served from memory. Add or change locales without touching the library.
- Composable — templates nest without limit: weighted choices, character classes and sub-templates combine to model any format.
- Reproducible — seed a faker and it emits the same sequence every time.
- Zero dependencies — standard library only.
Install
go get github.com/Timewave-AB/fakes
Requires Go 1.26+.
Usage
Point New at a locale directory, then generate values by category path with
Fake. The first path segment names a category (a JSON file); deeper dotted
segments descend into it.
package main
import (
"fmt"
"log"
"github.com/Timewave-AB/fakes"
)
func main() {
f, err := fakes.New("./locales/sv_SE")
if err != nil {
log.Fatal(err)
}
for _, path := range []string{"person", "address", "phone", "address.locality"} {
v, err := f.Fake(path)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%-18s %s\n", path, v)
}
}
person Sara Eriksson
address Kungsvägen 68
379 17 Stockholm
phone 072-402 91 67
address.locality Linköping
Seed a faker for reproducible output — same seed + locale yields an identical sequence, handy for stable tests:
a, _ := fakes.New("./locales/sv_SE", fakes.WithSeed(42))
b, _ := fakes.New("./locales/sv_SE", fakes.WithSeed(42))
av, _ := a.Fake("person")
bv, _ := b.Fake("person")
av == bv // true
A *Fakes is not safe for concurrent use — create one per goroutine.
Locales
The library ships a ready-to-use set under locales/ (en_US,
sv_SE). Pass New the path to one of them, a copy, or your own directory —
anywhere on disk.
The directory's name is the locale and must be a full tag (sv_SE, never
sv). Any casing or separator is accepted and canonicalised (sv-se,
SV_SE → sv_SE); a non-full name returns an error.
Locale data format
Each JSON file in a locale directory is a category named after the file
(address.json → address), rendered by Fake("address"). Drop in a new file
or folder — no code change, no recompile.
Every value is a node, one of three shapes, nestable without limit:
| Node | JSON | Meaning |
|---|---|---|
| literal | "Malmö" |
emitted verbatim — never formatted |
| choice | ["a", "b", …] |
one element, picked at random |
| template | {"format": "…", …} |
a format string plus the named sub-nodes it references |
Weight. A template node may carry a weight (default 1) to skew its odds
within a choice:
[
{ "format": "#070-000 00 00", "weight": 10 },
{ "format": "#01-000 00 00" },
{ "format": "#010-000 00 00" }
]
Format string. Every character is literal except:
| Token | Expands to |
|---|---|
0 |
digit 0–9 |
1 |
digit 1–9 |
A |
letter A–Z |
a |
letter a–z |
# |
escape — the next char is literal (#0 → 0, ## → #) |
{name} |
render the sibling field name |
{a|b} renders one of the sibling fields a or b, chosen at random.
Putting it together (person.json):
[
{
"format": "{prefix}{femalefirst|malefirst} {last}",
"femalefirst": ["Anna", "Astrid", "Elin"],
"malefirst": ["Anders", "Erik", "Gustav"],
"last": [
{ "format": "{first}sson", "first": ["Ander", "Erik", "Karl"] },
["Berg", "von Flemming"]
],
"prefix": [
"",
{ "format": "{string} ", "string": ["dr", "prof"], "weight": 0.05 }
]
}
]
This yields e.g. Anna Eriksson, Erik Berg, or rarely dr Astrid von Flemming.
Any field is reachable by dotted path — Fake("person.last") renders just a
surname; choices along the path are resolved at random.
Performance
Each file is parsed, validated and weight-indexed once, in New. After that a
Fake call costs about what its output costs — it scans the chosen format and
renders nested tokens, independent of how large your lists are:
- Picking from a list is O(1) whatever its length — a 10-name list and a 100 000-name list cost the same.
- Giving entries a
weightmakes that list's pick O(log n) instead (a search over cumulative weights). Still tiny, but an unweighted list is the cheapest — only addweightwhere you actually want skew. - Long
formatstrings, deep nesting and many{tokens}add cost in proportion to the output produced.
Development
Everything runs in Docker — no local tooling beyond Docker is needed.
Source is bind-mounted; build caches persist in the gocache volume.
docker compose run --rm test # run tests
docker compose run --rm ci # vet + format check + tests
docker compose run --rm cover # tests with coverage
docker compose run --rm build # compile the library
docker compose run --rm vet # go vet
docker compose run --rm dev # interactive shell
Commands that rewrite source keep your file ownership when run with --user:
docker compose run --rm --user "$(id -u):$(id -g)" fmt # gofmt -w .
docker compose run --rm --user "$(id -u):$(id -g)" tidy # go mod tidy
docker build . runs go vet and the tests, so it works as a CI gate too.
Layout
fakes.go Fakes, New, options, seeding
template.go Fake, the recursive renderer (choices, format strings, paths)
locale.go locale loading + tag parsing
locales/ shipped locale data (JSON)
To add a category, drop a JSON file into a locale directory; to add a locale, add a directory named with its full tag.
License
MIT — see LICENSE.