regextra

package module
v2.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 2, 2026 License: MIT Imports: 13 Imported by: 0

README

regextra

Go Reference Tests

Extensions to Go's regexp package for easier handling of named capture groups.

Installation

go get github.com/jecoms/regextra/v2@latest

Stability

regextra is at v2 and follows strict SemVer. Import it at the /v2 module path (github.com/jecoms/regextra/v2); the v1 line is frozen at the v1.x tags.

  • Breaking changes ship in the next major version (v3.0.0), never in a minor or patch.
  • Minor releases (v2.x.0) add features.
  • Patch releases (v2.x.y) are fixes only.

v2.0.0 collects the behavior changes made since v1.0.0; see CHANGELOG.md for the exhaustive list, with every breaking entry marked.

The forward look is tracked in the issue tracker.

What counts as breaking

  • Removing or renaming an exported symbol.
  • Changing the signature of an exported function or method.
  • Changing observable behavior of an existing call (e.g. a previously-returning call now returns an error).

What does not count as breaking

  • Adding a new exported function, type, or method.
  • Adding a new option to the regex:"..." struct tag grammar.
  • Accepting additional field types in Unmarshal / UnmarshalAll / Decoder.
  • Changing the wording of error messages. Do not pattern-match on err.Error() strings; compare against the exported sentinels (e.g. regextra.ErrNoMatch, regextra.ErrInvalidPattern, regextra.ErrInvalidStruct with errors.Is) or recover the typed *regextra.DecodeError with errors.As instead.

Usage

package main

import (
    "fmt"
    "regexp"
    "github.com/jecoms/regextra/v2"
)

func main() {
    re := regexp.MustCompile(`(?P<name>\w+) (?P<age>\d+)`)
    
    // Extract a single named group
    name, ok := regextra.FindNamed(re, "Alice 30", "name")
    if ok {
        fmt.Println("Name:", name) // Output: Name: Alice
    }
    
    // Get all named groups as a map
    groups := regextra.NamedGroups(re, "Alice 30")
    fmt.Println(groups) // Output: map[age:30 name:Alice]
    
    // Unmarshal into a struct with type conversion
    type Person struct {
        Name string
        Age  int
    }
    var person Person
    regextra.Unmarshal(re, "Bob 25", &person)
    fmt.Printf("%s is %d\n", person.Name, person.Age) // Output: Bob is 25
    
    // Unmarshal all matches into a slice
    var people []Person
    regextra.UnmarshalAll(re, "Alice 30 and Bob 25", &people)
    fmt.Println(len(people)) // Output: 2
}

API

FindNamed(re *regexp.Regexp, target, groupName string) (string, bool)

Extract a single named capture group from the target string.

Returns the matched value and true if found, or empty string and false if not found.

re := regexp.MustCompile(`(?P<price>\$\d+\.\d{2})`)
price, ok := regextra.FindNamed(re, "Total: $19.99", "price")
// price = "$19.99", ok = true
FindAllNamed(re *regexp.Regexp, target, groupName string) []string

Extract every value of a single named capture group across all matches.

Returns nil if the group name is not declared on the regex; an empty slice if the group is declared but the regex has no matches.

re := regexp.MustCompile(`(?P<word>\S+)`)
words := regextra.FindAllNamed(re, "alpha beta gamma", "word")
// words = []string{"alpha", "beta", "gamma"}

For a single match, prefer FindNamed. To pull every named group from one match — including patterns where the same name is declared more than once — use AllNamedGroups. Despite the "All" prefix, AllNamedGroups operates on a single match; it is not the all-matches counterpart of FindAllNamed.

NamedGroups(re *regexp.Regexp, target string) map[string]string

Extract all named capture groups as a map. If a group name appears multiple times (e.g. across alternation branches), the value of the last occurrence that participated in the match wins; a non-participating occurrence never overwrites a participating one.

Returns an empty map if no match is found.

re := regexp.MustCompile(`(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})`)
groups := regextra.NamedGroups(re, "Date: 2025-10-04")
// groups = map[string]string{"year": "2025", "month": "10", "day": "04"}
AllNamedGroups(re *regexp.Regexp, target string) map[string][]string

Operates on a single match and returns every value of every named capture group, keyed by group name. Each value is a slice because Go's regexp allows the same group name to appear more than once in a pattern — AllNamedGroups preserves every occurrence in left-to-right order. Groups that appear once still get a one-element slice.

The leading "All" refers to all named groups in one match — not to all matches across the target. Use FindAllNamed to collect a single named group across every match, or NamedGroupsPerMatch to collect every named group across every match as one map per match ([]map[string]string); the unmarshal path (UnmarshalAll, Decoder.All, Decoder.Iter) is the typed equivalent.

Returns an empty map if no match is found.

// Duplicate group names — the use case this function exists for:
re := regexp.MustCompile(`(?P<word>\w+) (?P<word>\w+) (?P<word>\w+)`)
allGroups := regextra.AllNamedGroups(re, "one two three")
// allGroups = map[string][]string{"word": []string{"one", "two", "three"}}

// Distinct group names — each slice has one element:
re = regexp.MustCompile(`(?P<name>\w+) (?P<age>\d+)`)
allGroups = regextra.AllNamedGroups(re, "Alice 30")
// allGroups = map[string][]string{"name": []string{"Alice"}, "age": []string{"30"}}
NamedGroupsPerMatch(re *regexp.Regexp, target string) []map[string]string

The every-match counterpart to NamedGroups: returns one map of named-group values per match of re in target, in match order. Each map follows the same per-match semantics as NamedGroups — every declared group is present, a group that did not participate in that match is mapped to "", and a reused group name resolves to the last participating occurrence in that match.

Returns an empty (non-nil) slice if there are no matches. For the typed equivalent that decodes each match into a struct, use UnmarshalAll / Decoder.All.

re := regexp.MustCompile(`(?P<key>\w+)=(?P<value>\w+)`)
all := regextra.NamedGroupsPerMatch(re, "a=1 b=2")
// all = []map[string]string{{"key": "a", "value": "1"}, {"key": "b", "value": "2"}}
NamedGroupsPerMatchSeq(re *regexp.Regexp, target string) iter.Seq[map[string]string]

The lazy, range-over-func (Go 1.23+) form of NamedGroupsPerMatch: yields one named-group map per match, in match order, without building the intermediate slice. Stopping the range early (break) stops the iteration. On no match, the iterator yields zero times. For the typed streaming equivalent, use Decoder.Iter.

re := regexp.MustCompile(`(?P<key>\w+)=(?P<value>\w+)`)
for m := range regextra.NamedGroupsPerMatchSeq(re, "a=1 b=2") {
    fmt.Println(m["key"], m["value"])
}
// a 1
// b 2
Replace(re *regexp.Regexp, target string, replacements map[string]string) string

Substitute the matched span of each named capture group with the value from replacements, leaving non-matching text and any groups absent from the map unchanged. Replace operates on every match of re, in order.

re := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>[\w.]+)`)
out := regextra.Replace(re, "alice@example.com bob@other.org", map[string]string{
    "domain": "redacted",
})
// out = "alice@redacted bob@redacted"
ReplaceFirst(re *regexp.Regexp, target string, replacements map[string]string) string

Like Replace, but substitutes named-group spans only within the first match of re; every later match, and all text outside the first match, passes through byte-for-byte unchanged. Within that first match it follows Replace's rules exactly (groups absent from the map pass through, non-participating groups are skipped, outermost span wins on overlap). On no match, the target is returned unchanged.

re := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>[\w.]+)`)
out := regextra.ReplaceFirst(re, "alice@example.com bob@other.org", map[string]string{
    "domain": "redacted",
})
// out = "alice@redacted bob@other.org"
ReplaceFunc(re *regexp.Regexp, target string, fn func(group, match string) string) string

Like Replace, but the replacement for each named-group span is computed by a callback over the matched value instead of looked up in a static map. Use it when the substitution depends on what matched — redaction, normalization, and similar. fn is called once per substituted named span, left to right, with the group's name and matched text; return the match verbatim to leave a group unchanged. On no match the target is returned unchanged and fn is never called. Passing a nil fn is a programmer error and panics on the first match, mirroring the standard library's Regexp.ReplaceAllStringFunc.

When named groups overlap (nesting), the outermost group whose span is encountered first wins and fn is not called for inner groups inside an already-substituted span — the same overlap rule as Replace.

// Mask all but the last four digits of a captured card number:
re := regexp.MustCompile(`(?P<card>\d{12,19})`)
out := regextra.ReplaceFunc(re, "card 4111111111111111 ok", func(group, match string) string {
    return strings.Repeat("*", len(match)-4) + match[len(match)-4:]
})
// out = "card ************1111 ok"
Validate(re *regexp.Regexp, required ...string) error

Returns an error listing every required group name that is not declared on re. Use it for init-time assertions in services that compile patterns once: catch typos at startup rather than at the first (mis-)matched request.

re := regexp.MustCompile(`(?P<name>\w+) (?P<age>\d+)`)

if err := regextra.Validate(re, "name", "age", "ssn"); err != nil {
    // err: regextra.Validate: missing named groups: ssn
}

On failure Validate returns an errors.As-able *regextra.MissingNamedGroupsError whose Missing field carries the absent group names (in the order passed), so you can branch on the missing set without parsing the message — see §Stability on comparing types, not strings:

var ve *regextra.MissingNamedGroupsError
if errors.As(err, &ve) {
    // ve.Missing == []string{"ssn"}
}
Unmarshal(re *regexp.Regexp, target string, v any) error

Unmarshal regex matches into a struct with automatic type conversion. Similar to json.Unmarshal, but for regex patterns.

Supported field types: string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool, time.Time, time.Duration. Pointer-to-any-of-the-above is also supported — nil pointers are allocated, non-nil pointers are reused (pointee overwritten). For time.Time, several common layouts are tried in order (RFC3339Nano, RFC3339, 2006-01-02 15:04:05, 2006-01-02, 15:04:05); time.Duration is parsed via time.ParseDuration. Any field whose type (or pointer-to-type) implements encoding.TextUnmarshaler is also supported out of the box — e.g. netip.Addr, math/big.Int, log/slog.Level, github.com/google/uuid.UUID — by calling its UnmarshalText with the matched value. For caller-defined types, implement RegexUnmarshaler.

Field mapping priority:

  1. Struct tag regex:"groupname" if provided (highest priority)
  2. Exact field name match with capture group name
  3. Case-insensitive field name match (Unicode simple case folding)
  • Unexported fields are ignored

Returns an error if the target is not a pointer to a struct, or if type conversion fails.

type Person struct {
    Name string
    Age  int
}

re := regexp.MustCompile(`(?P<name>\w+) is (?P<age>\d+)`)
var person Person
err := regextra.Unmarshal(re, "Alice is 30", &person)
// person.Name = "Alice", person.Age = 30

With struct tags:

type Email struct {
    Username string `regex:"user"`
    Domain   string `regex:"domain"`
}

re := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>[\w.]+)`)
var email Email
err := regextra.Unmarshal(re, "alice@example.com", &email)
// email.Username = "alice", email.Domain = "example.com"

Tag options:

The regex:"..." tag accepts comma-separated key=value options after the group name:

Option Applies to Effect
default=<value> Any field type Substituted when the named group is not declared on the regex or its match is empty. The default goes through the same type conversion as a real match.
layout=<go-time-layout> time.Time only Use the supplied time.Parse layout exclusively, instead of the default fallback list. Lets you pin the parser to (e.g.) Apache, syslog, or any other non-RFC3339 timestamp shape.
required (flag) Any field type Decode fails with an errors.As-able *RequiredGroupError when the named group does not participate in the match or matches an empty span and no default= supplies a value. A default= satisfies the requirement. Lets a field declare its mandatory-ness inline instead of a separate Validate pass.
type LogLine struct {
    TS    time.Time `regex:"ts,layout=02/Jan/2006:15:04:05 -0700"`
    Level string    `regex:"level,default=info"`
    User  string    `regex:"user,required"`
}

Excluding a field: regex:"-" excludes a field entirely — it is never populated, even if a declared group happens to share the field's name. This matches the - convention in encoding/json, encoding/xml, and gopkg.in/yaml. It differs from an absent tag (regex:""), which falls back to matching the field's own name against a group. Only the bare - excludes; a leading - followed by options (e.g. regex:"-,default=x") parses - as the group name, which matches no group since regexp group names are Go identifiers.

Forward-compat rules (v1 contract):

  • Unknown key=value pairs are preserved, not rejected. Adding a new option key in a future minor release is not a breaking change. Don't rely on the parser rejecting unknown keys — pin a minor version range if you need a specific recognized set.
  • Lone tokens (no =) other than the recognized required flag are silently ignored. Today, regex:"name,foo" parses as (name="name") — the foo token is dropped. The slot is reserved for future flag-style options (required claimed the first one — see the options table above); a later minor may start recognizing further lone tokens. Don't rely on an unrecognized lone token remaining inert.

See the package doc's Tag grammar section on pkg.go.dev for the canonical statement.

RegexUnmarshaler interface

Mirror of encoding.TextUnmarshaler for regextra's unmarshal path. When a destination field's pointer type satisfies this interface, Unmarshal (and UnmarshalAll) call UnmarshalRegex with the matched group value instead of running the built-in string/int/uint/float/bool conversion.

type RegexUnmarshaler interface {
    UnmarshalRegex(value string) error
}

The extension point for caller-defined types the built-in type switch can't handle (URLs, enums, big numbers, IP addresses, etc.).

Conversion precedence. For each field, Unmarshal tries in order: (1) RegexUnmarshaler — the package-specific hook always wins; (2) the time.Time / time.Duration special-cases (so the multi-layout fallback and layout tag option are preserved — time.Time happens to implement encoding.TextUnmarshaler but its UnmarshalText only accepts RFC3339, so it is not routed through the fallback); (3) encoding.TextUnmarshaler — for any other type that implements it; (4) the built-in string/int/uint/float/bool conversion. So a type implementing both RegexUnmarshaler and encoding.TextUnmarshaler dispatches on UnmarshalRegex.

type Status int

const (
    StatusUnknown Status = iota
    StatusOpen
    StatusClosed
)

func (s *Status) UnmarshalRegex(value string) error {
    switch value {
    case "open":   *s = StatusOpen
    case "closed": *s = StatusClosed
    default:       return fmt.Errorf("unknown status %q", value)
    }
    return nil
}

type Issue struct {
    ID    int    `regex:"id"`
    State Status `regex:"state"`
}

re := regexp.MustCompile(`#(?P<id>\d+) \[(?P<state>\w+)\]`)
var issue Issue
regextra.Unmarshal(re, "#42 [open]", &issue)
// issue = Issue{ID: 42, State: StatusOpen}
UnmarshalAll(re *regexp.Regexp, target string, v any) error

UnmarshalAll finds all occurrences of the regex pattern in the target string and unmarshals them into a slice of structs. The slice is cleared before populating.

v must be a pointer to a slice of structs. If no matches are found, the slice will be empty.

Supported field types: Same as Unmarshal

Field mapping priority: Same as Unmarshal

type Person struct {
    Name string
    Age  int
}

re := regexp.MustCompile(`(?P<name>\w+) is (?P<age>\d+)`)
var people []Person
err := regextra.UnmarshalAll(re, "Alice is 30 and Bob is 25", &people)
// people = []Person{
//     {Name: "Alice", Age: 30},
//     {Name: "Bob", Age: 25},
// }
Compile[T any](pattern string) (*Decoder[T], error) / MustCompile[T any](pattern string) *Decoder[T]

Typed, regex-bound unmarshaler that carries the precomputed reflect plan for T's fields. Compile once, decode many times. (Unmarshal / UnmarshalAll cache their decode plan too — built on first use of a (pattern, struct type) pair, kept for the life of the process — so the Decoder's remaining edge is skipping the per-call cache lookup, plus the strict upfront validation below.)

type Person struct {
    Name string `regex:"name"`
    Age  int    `regex:"age"`
}

// Compile validates pattern + struct tags upfront.
var personDecoder = regextra.MustCompile[Person](`(?P<name>\w+) is (?P<age>\d+)`)

// Hot path — no reflect per call.
p, err := personDecoder.One("Alice is 30")
// p = Person{Name: "Alice", Age: 30}, err = nil

people, _ := personDecoder.All("Alice is 30 and Bob is 25")
// people = []Person{{"Alice", 30}, {"Bob", 25}}

vs Unmarshal: because the free functions cache their decode plan after the first call, the steady-state gap is small — the same simple-struct shape benchmarks at ~265 ns/op (2 allocs) via Decoder versus ~290 ns/op (2 allocs) via Unmarshal on Apple M4 (measurements are hardware-dependent, not a guarantee). Decoder's remaining edge is skipping the per-call cache lookup — and, more importantly, Compile validates pattern/tag agreement strictly up front, where the lenient free functions skip misbound fields silently. Use Decoder when you'll decode the same shape many times (log parsers, config readers, request handlers); use Unmarshal for one-shot extraction.

Compile-time validation is strict. Compile returns an error (or MustCompile panics) if:

  • The pattern is not a valid regex
  • T is not a struct
  • A field's regex:"name" tag references a group not declared on the pattern (unless paired with default=)
  • A default= value cannot be converted to its field type
  • A layout= option is on a non-time.Time field

This is the strictness you want for "compile once" — typos fail at startup, not at first request.

Each failure is categorized by a wrapped sentinel so you can branch on the kind with errors.Is instead of parsing the message: regextra.ErrInvalidPattern for the bad-regex case, and regextra.ErrInvalidStruct for the four destination-shape cases. MustCompile panics with the same wrapped error. The sentinels are Compile-only — the lenient Unmarshal / UnmarshalAll path never surfaces them.

if _, err := regextra.Compile[Person](pattern); err != nil {
    switch {
    case errors.Is(err, regextra.ErrInvalidPattern):
        // bad regular expression
    case errors.Is(err, regextra.ErrInvalidStruct):
        // struct/tag shape problem
    }
}

Decoder.One returns regextra.ErrNoMatch (compare with errors.Is) when there's no match. Other errors indicate per-field conversion failure on a successful match.

Typed conversion failures. When a matched group value can't be converted to its field type, every decode entrypoint (Unmarshal, UnmarshalAll, Decoder.One/All/Iter) returns a *regextra.DecodeError carrying the field name, capture group, raw value, target type, and wrapped cause. Recover it with errors.As to branch without parsing message text:

var de *regextra.DecodeError
if errors.As(err, &de) {
    log.Printf("field %s (group %s): cannot parse %q as %s", de.Field, de.Group, de.Value, de.Type)
}

Required groups. A field tagged regex:",required" (see the Tag options table under Unmarshal) must receive a value: when its group does not participate in the match or matches an empty span and no default= supplies one, the same decode entrypoints return a *regextra.RequiredGroupError carrying the field name and capture group. It is the per-match presence check, complementing *DecodeError (a value that failed conversion) and *MissingNamedGroupsError (the static Validate check that the pattern declares a group at all):

var rge *regextra.RequiredGroupError
if errors.As(err, &rge) {
    log.Printf("field %s (group %s) is required but had no value", rge.Field, rge.Group)
}

Constructed errors are prefixed with the entrypoint that produced them (regextra.Unmarshal:, regextra.Decoder.One:, …); the bare regextra: prefix is reserved for package-level sentinels like ErrNoMatch, ErrInvalidPattern, and ErrInvalidStruct. One intended exception: the construction-time errors from Compile / MustCompile and Decoder.Encoder() carry the bare regextra: prefix rather than a regextra.Compile: / regextra.Decoder.Encoder: entrypoint prefix, because they wrap the ErrInvalidPattern / ErrInvalidStruct / ErrNotInvertible sentinels directly — so a regextra:-prefixed message there is by design, not a missing prefix. Treat these prefixes as informational — compare against the sentinel or the *DecodeError type, not the string.

Streaming with Decoder.Iter:

// Pair each match with its decode error so callers can skip individual failures
// without aborting the whole iteration. Break freely to stop early.
for v, err := range personDecoder.Iter(input) {
    if err != nil {
        log.Printf("skipping bad match: %v", err)
        continue
    }
    process(v)
}

Iter returns an iter.Seq2[T, error] (Go 1.23+ range-over-func). Use it for streaming-style consumption (log parsers, scrapers) where you don't want to materialize the full result slice up-front. On a full 100-line corpus its throughput is roughly comparable to UnmarshalAll (≈28 µs/op, ~205 allocs/op vs ≈26 µs/op, ~114 allocs/op on Apple M4 — hardware-dependent, not a guarantee); Iter's win is streaming, not raw speed. It skips allocating the full result slice (lower peak memory on large inputs) and decodes lazily. Match-finding still happens in one regex call (Go's stdlib doesn't expose a streaming-find API), but the per-match decode work IS lazy — break in the range body avoids decoding the remaining matches.

Accessors. A Decoder exposes the pattern it was compiled from, so you don't have to keep a copy alongside it:

  • Pattern() string returns the regex source string — handy for logging and debugging.
  • Regexp() *regexp.Regexp returns the underlying compiled *regexp.Regexp, so you can reuse it for your own match-finding (e.g. FindAllIndex, custom iteration) without recompiling. The returned pointer is shared with the Decoder: its exported methods are read-only and safe for concurrent use, but do not mutate shared matcher state on it — in particular don't call Longest(), which changes matching semantics for the Decoder too.
dec := regextra.MustCompile[Person](`(?P<name>\w+) is (?P<age>\d+)`)
dec.Pattern()          // "(?P<name>\\w+) is (?P<age>\\d+)"
dec.Regexp().FindAllString("Alice is 30 and Bob is 25", -1)
// []string{"Alice is 30", "Bob is 25"}

Decoder instances are safe for concurrent use.

(d *Decoder[T]) Encoder() (*Encoder[T], error)

The typed inverse of Decoder, derived from the decoder's own compiled pattern — write the pattern once and get the encoder for free, with no separate template to keep in sync. Encode followed by a Decoder.One / Unmarshal on the same pattern round-trips the original struct.

Encoder() parses the decoder's pattern with regexp/syntax and inverts the invertible subset of the grammar into an ordered encode plan:

  • Literal text is emitted verbatim (regexp escapes like \. are already decoded by the parser).
  • Named capture groups (?P<name>…) become field substitutions: name resolves to a struct field with the same rules Decoder uses (the field's regex:"name" tag if present, otherwise the field's own name, matched case-insensitively; a regex:"-" field is excluded). The group's sub-pattern is discarded — the field's value fills the span.
  • Anchors and zero-width assertions (^, $, \A, \z, \b, …) match no text and are dropped.
  • An unnamed group whose body is pure literal text is treated as that literal.
type Person struct {
    Name string `regex:"name"`
    Age  int    `regex:"age"`
}

dec := regextra.MustCompile[Person](`(?P<name>\S+) is (?P<age>\d+)`)
enc, _ := dec.Encoder()

s, _ := enc.Encode(Person{Name: "Alice", Age: 30})   // "Alice is 30"
back, _ := dec.One(s)                                 // Person{Name: "Alice", Age: 30}

Non-invertible patterns fail fast. Any construct with no single string to emit — an alternation (|), a quantifier (*, +, ?, {n,m}), a character class ([...]), an any-character wildcard (.), or an unnamed group with non-literal content — appearing outside a named capture group makes the pattern non-invertible, and Encoder() returns an error wrapping regextra.ErrNotInvertible that names the offending construct. (Inside a named capture such constructs are fine: the field's value fills the group.)

Supported field types: same set as Unmarshalstring, all int/uint/float widths, bool, time.Time, time.Duration, and single-level pointers to any of these. time.Time encodes as RFC3339Nano by default (the first layout Decoder tries, so the output re-parses and sub-second precision survives), or the layout= layout when tagged. Any type implementing encoding.TextMarshaler (e.g. netip.Addr, uuid.UUID) is encoded via MarshalText. For caller-defined types, implement RegexMarshaler (below).

Construction-time validation is strict, mirroring Compile: Encoder() returns an error if the pattern is not invertible (above), a named group maps to no exported/eligible field, or a mapped field's type can't be encoded (the latter two wrap regextra.ErrInvalidStruct). A successful Encoder() can only fail at Encode time on a runtime value error (a custom marshaler returning an error, or a nil pointer field, which has no string form) — surfaced as a *regextra.EncodeError (the encode-side mirror of DecodeError).

Round-trip contract. Encode(v) re-decodes to v when each encoded value re-matches the sub-pattern of the group it fills — the caller owns that pairing by writing value-appropriate sub-patterns (a captured word wants \S+, not .*). The default= tag option does not affect encoding (it is a decode-side substitution); Encode always emits the field's actual value. Values that collide with a surrounding literal delimiter, or two adjacent captures with no literal between them, have no unambiguous decode boundary and are out of scope. (A future option is to re-match each encoded value against its group's sub-pattern at Encode time; that is deliberately not done today.)

Encoder instances are safe for concurrent use.

RegexMarshaler interface

The encode-side mirror of RegexUnmarshaler. When an Encoder field's type satisfies this interface, Encode calls MarshalRegex instead of the built-in conversion. A type implementing both RegexMarshaler and RegexUnmarshaler round-trips symmetrically through Encoder and Decoder.

type RegexMarshaler interface {
    MarshalRegex() (string, error)
}

Conversion precedence mirrors the decode side: (1) RegexMarshaler; (2) the time.Time / time.Duration special-cases; (3) encoding.TextMarshaler; (4) the built-in string/int/uint/float/bool conversion.

func (s Status) MarshalRegex() (string, error) {
    switch s {
    case StatusOpen:   return "open", nil
    case StatusClosed: return "closed", nil
    default:           return "", fmt.Errorf("unknown status: %d", s)
    }
}

Why regextra?

The standard library's regexp package requires verbose code to extract named capture groups:

// Standard library approach (verbose)
re := regexp.MustCompile(`(?P<name>\w+) (?P<age>\d+)`)
matches := re.FindStringSubmatch("Alice 30")
if matches != nil {
    nameIndex := re.SubexpIndex("name")
    name := matches[nameIndex]  // "Alice"
}

// regextra approach (simple)
re := regexp.MustCompile(`(?P<name>\w+) (?P<age>\d+)`)
name, ok := regextra.FindNamed(re, "Alice 30", "name")  // "Alice", true

Features

  • Simple functions - No wrapper types, works directly with *regexp.Regexp
  • Named group extraction - Extract groups by name without index juggling
  • Map-based access - Get all named groups in one call
  • Struct unmarshaling - Type-safe extraction with automatic conversion
  • Typed round-trip - Encoder[T] renders a struct back to a string, derived by inverting the decoder's own compiled pattern (no separate template to keep in sync)
  • Safe by default - Built-in nil checks, returns empty values on no match
  • Zero dependencies - Only depends on Go's standard library

License

MIT

Documentation

Overview

Package regextra adds the convenience layer the standard library's regexp package leaves out: name-based access to capture groups, struct unmarshaling, and a typed cached decoder for repeated patterns.

The pain it solves

Extracting a named group with stdlib regexp is a three-step dance:

re := regexp.MustCompile(`(?P<name>\w+) (?P<age>\d+)`)
matches := re.FindStringSubmatch("Alice 30")
nameIndex := re.SubexpIndex("name")
name := matches[nameIndex]  // "Alice"

regextra collapses that to one call, and goes further with map-based access and json.Unmarshal-style decoding into structs.

Quick start

re := regexp.MustCompile(`(?P<name>\w+) is (?P<age>\d+)`)

// Single named group:
name, ok := regextra.FindNamed(re, "Alice is 30", "name")  // "Alice", true

// All named groups as a map:
m := regextra.NamedGroups(re, "Alice is 30")  // map[name:Alice age:30]

// Decode into a typed struct:
type Person struct {
    Name string
    Age  int
}
var p Person
regextra.Unmarshal(re, "Alice is 30", &p)  // p = {Name: "Alice", Age: 30}

API at a glance

By use case:

Performance

Every decode path caches its field-mapping reflect work. Unmarshal / UnmarshalAll build the per-field decode plan on first use of a (pattern, struct type) pair and reuse it from an internal package-level cache on later calls, so repeated free-function decode pays only a cache lookup on top of the match itself. The cache lives for the process and is never evicted — the same trade encoding/json makes for its field cache: entries are small (they do not retain the compiled regexp), and real workloads use a bounded set of patterns and types. Compile / Decoder remains the right tool for repeated decode of the same shape (log parsers, request handlers, config readers): the Decoder carries its own plan, so it skips even the cache lookup, and its strict compile-time validation surfaces tag typos at startup rather than never (the lenient free functions skip misbound fields silently). Decoder.Iter further skips the slice allocation entirely for streaming consumers.

No-match behavior

Functions in this package handle "the regex did not match the target" differently depending on their return shape. The asymmetry is intentional — each call returns the no-match form that lets the caller continue without a special-case branch.

Function                                  No-match return
----------------------------------------------------------------------------
FindNamed                                 ("", false)
FindAllNamed                              []string{} (or nil if the group
                                          name is not declared on the regex)
NamedGroups, AllNamedGroups               empty map (initialized, not nil)
NamedGroupsPerMatch                       []map[string]string{} (empty, not nil)
NamedGroupsPerMatchSeq                    iterator yields zero times
Replace                                   target returned unchanged
ReplaceFirst                              target returned unchanged
ReplaceFunc                               target returned unchanged (fn
                                          never called)
Validate                                  unrelated — checks declarations,
                                          not matches against a target
Unmarshal                                 nil error; destination struct left
                                          unchanged
UnmarshalAll                              nil error; destination slice
                                          length set to 0
Decoder.One                               zero T, [ErrNoMatch]
Decoder.All                               []T{}, nil
Decoder.Iter                              iterator yields zero times

The contrast worth understanding is between Unmarshal and Decoder.One:

  • Unmarshal returns nil on no match. The caller passes the destination, so they can inspect their own struct after the call to detect "did anything decode?" — no sentinel needed, and reserving error for genuine failures (bad pointer, type-conversion failure) keeps `if err != nil` meaningful.

  • Decoder.One returns ErrNoMatch. It returns (T, error), constructing the value itself; a zero-value T paired with nil error would be indistinguishable from "successfully decoded a struct of all zero fields". The sentinel disambiguates. Compare with errors.Is so the check survives wrapping.

Example of the Decoder.One no-match check:

v, err := dec.One(input)
if errors.Is(err, regextra.ErrNoMatch) {
    // no match — handle as data absence, not failure
}

Decoder.All and Decoder.Iter don't have the ambiguity problem — an empty slice and a zero-iteration range are unambiguous — so they follow the same "no match is not an error" convention as UnmarshalAll.

For FindAllNamed, the nil-vs-empty-slice split is a separate signal:

  • nil — the group name is not declared on the regex (likely a typo; consider Validate at startup to catch this).
  • []string{} — the group is declared but the regex has no matches in the target (data absence — iterate over zero or more).

This distinction is the only place in the no-match table where a single function returns two different no-match shapes; everywhere else the no-match form is fixed regardless of why the match failed.

Tag grammar

The `regex:"..."` struct tag uses a JSON-encoding-style grammar: the first comma-separated piece is the group name; each subsequent piece is a key=value option. Currently recognized keys:

default=<value>           Any field type. Substituted when the named
                          group is undeclared on the regex or its match
                          is empty. Goes through the same type
                          conversion as a real match.
layout=<go-time-layout>   time.Time only. Used exclusively, instead of
                          the default RFC3339-and-friends fallback list.

The grammar also recognizes one flag-style token (no `=`):

required                  Decode fails with a *RequiredGroupError when
                          the named group does not participate in the
                          match or matches an empty span and no default=
                          supplies a value. A default= satisfies the
                          requirement, since it always yields a value.

The two "empty" forms differ, matching the convention in encoding/json, encoding/xml, and gopkg.in/yaml:

regex:""    No tag. Fall back to the field's own name for matching.
regex:"-"   Exclude the field entirely. It is never populated, even if a
            declared group happens to share its name.

Only the bare `-` tag excludes. A leading `-` followed by options (e.g. `regex:"-,default=x"`) parses `-` as the group name, which matches no group since regexp group names are Go identifiers.

Two forward-compatibility rules in the tag parser are part of the v1 contract:

  • Unknown key=value pairs are preserved, not rejected. The parser stores every key=value pair regardless of whether the key is currently recognized, so a future minor release can introduce additional option keys without a parser change. Adding a new option key is therefore not a breaking change. Callers must not rely on the parser rejecting unknown keys; pin a minor version range if you need a specific recognized set.

  • Lone tokens (no `=`) other than the recognized `required` flag are silently ignored. Today, `regex:"name,foo"` is a no-op — the `foo` token is dropped, so the field resolves exactly as `regex:"name"` would. This slot is reserved for future flag-style options (the `required` flag above claimed the first one; see the issue tracker at https://github.com/Jecoms/regextra/issues). A later minor release may start recognizing further lone tokens and giving them meaning, so adding `regex:"name,foo"` today is a no-op but may stop being one. Callers must not rely on an unrecognized lone token remaining inert.

The two rules together are how the tag grammar grows compatibly: a new option ships as either an additional key=value pair (orthogonal to today's grammar) or a recognized flag token (claiming a previously-ignored slot).

Stability

regextra is at v1 and follows strict SemVer. Patch releases are fixes only. Minor releases add features without breaking changes. Breaking changes ship in the next major version, never in a minor or patch. See the README's Stability section for the precise contract, including what does and does not count as breaking.

More

The package README has full per-function reference with runnable examples for each function.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidPattern = errors.New("regextra: invalid pattern")
	ErrInvalidStruct  = errors.New("regextra: invalid struct")
)

ErrInvalidPattern and ErrInvalidStruct categorize Compile (and therefore MustCompile) failures so callers can branch on the failure kind with errors.Is rather than parsing the message. ErrInvalidPattern wraps a bad regular expression; ErrInvalidStruct wraps every destination-shape problem (T is not a struct, a field references an undeclared group, a `default=` value does not convert, or `layout=` sits on a non-time.Time field). Each wrapped error keeps its descriptive detail — and, where one exists, the underlying cause — reachable via errors.Is/As. Like ErrNoMatch, these sentinels carry the bare `regextra:` prefix reserved for package-level sentinels.

View Source
var ErrNoMatch = errors.New("regextra: no match")

ErrNoMatch is returned by Decoder.One when the target string does not match the regex. Compare with errors.Is so callers can distinguish "no match" from genuine decoding failures.

View Source
var ErrNotInvertible = errors.New("regextra: pattern is not invertible")

ErrNotInvertible categorizes a Decoder.Encoder failure where the decoder's pattern contains a construct that has no single string to emit when encoding — an alternation (`|`), a quantifier (`*`, `+`, `?`, `{n,m}`), a character class (`[...]`), an any-character wildcard (`.`), or an unnamed group with non-literal content — appearing outside a named capture group. Callers can branch on the failure kind with errors.Is rather than parsing the message. Like ErrNoMatch and ErrInvalidPattern, it carries the bare `regextra:` prefix reserved for package-level sentinels.

Inside a named capture group such constructs are fine: the struct field's value fills the group, so the sub-pattern describing what the group matches is irrelevant to encoding.

Functions

func AllNamedGroups

func AllNamedGroups(re *regexp.Regexp, target string) map[string][]string

AllNamedGroups operates on a single match and returns every value of every named capture group, keyed by group name. Each value is a slice because Go's regexp package allows the same group name to appear more than once in a pattern; AllNamedGroups preserves every occurrence in left-to-right order. Groups that appear once still get a one-element slice.

The leading "All" refers to all named groups in one match — not to all matches across the target. Internally the function inspects only the first match, so only the first match contributes values. To collect every value of a single named group across every match in the target, use FindAllNamed. To collect every named group across every match as one map per match, use NamedGroupsPerMatch (or NamedGroupsPerMatchSeq for the lazy form); the unmarshal path (UnmarshalAll, Decoder.All, Decoder.Iter) is the typed equivalent.

On no match, returns an empty (non-nil) map. See the package doc's "No-match behavior" section for the full cross-API contract.

Example — duplicate group names (the use case this function exists for):

re := regexp.MustCompile(`(?P<word>\w+) (?P<word>\w+)`)
allGroups := regextra.AllNamedGroups(re, "hello world")
// allGroups = map[string][]string{"word": []string{"hello", "world"}}

Example — distinct group names (each slice has one element):

re := regexp.MustCompile(`(?P<name>\w+) (?P<age>\d+)`)
allGroups := regextra.AllNamedGroups(re, "Alice 30")
// allGroups = map[string][]string{"name": []string{"Alice"}, "age": []string{"30"}}
Example
package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
	"regexp"
)

func main() {
	re := regexp.MustCompile(`(?P<word>\w+) (?P<word>\w+) (?P<word>\w+)`)
	allGroups := rx.AllNamedGroups(re, "one two three")

	fmt.Printf("word: %v\n", allGroups["word"])
}
Output:
word: [one two three]

func FindAllNamed

func FindAllNamed(re *regexp.Regexp, target, groupName string) []string

FindAllNamed returns every value of the named capture group across all matches of re in target. Returns nil if the group name is not declared on the regex; an empty slice if the group is declared but the regex has no matches.

Example:

re := regexp.MustCompile(`(?P<word>\S+)`)
words := regextra.FindAllNamed(re, "alpha beta gamma", "word")
// words = []string{"alpha", "beta", "gamma"}

For a single match, prefer FindNamed which returns (value, ok). To pull every named group from one match (with duplicate-name handling), use AllNamedGroups. Despite the "All" prefix, AllNamedGroups operates on a single match — it does not iterate matches across the target the way FindAllNamed does.

When the pattern reuses a group name, each match contributes the value of the occurrence that participated in that match, not blindly re.SubexpIndex's first occurrence.

Example
package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
	"regexp"
)

func main() {
	re := regexp.MustCompile(`(?P<word>\S+)`)
	words := rx.FindAllNamed(re, "alpha beta gamma", "word")
	fmt.Println(words)
}
Output:
[alpha beta gamma]

func FindNamed

func FindNamed(re *regexp.Regexp, target, groupName string) (string, bool)

FindNamed returns the value of the named capture group in the target string. It returns the matched value and true if found, or empty string and false if not found.

When the pattern reuses a group name (e.g. across alternation branches), FindNamed returns the value of the last occurrence that participated in the match — it does not trust re.SubexpIndex, which reports only the first occurrence and would return the wrong value when a later branch is the one that matched.

Example:

re := regexp.MustCompile(`(?P<name>\w+) (?P<age>\d+)`)
name, ok := regextra.FindNamed(re, "Alice 30", "name")
// name = "Alice", ok = true
Example
package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
	"regexp"
)

func main() {
	re := regexp.MustCompile(`(?P<name>\w+) (?P<age>\d+)`)
	name, ok := rx.FindNamed(re, "Alice 30", "name")
	fmt.Printf("%s: %v\n", name, ok)
}
Output:
Alice: true

func NamedGroups

func NamedGroups(re *regexp.Regexp, target string) map[string]string

NamedGroups returns a map of all named capture groups and their matched values from the target string. If no match is found, it returns an empty map.

When the pattern reuses a group name (e.g. across alternation branches), the value of the last occurrence that participated in the match wins; an occurrence that did not participate never overwrites a participating occurrence's value. A declared group that did not participate at all is still present in the map, mapped to "".

To see every occurrence rather than just the winning one, use AllNamedGroups — but note it reports a non-participating occurrence and an occurrence that matched an empty span identically (both as ""), so it cannot be used to tell those two cases apart.

Example:

re := regexp.MustCompile(`(?P<name>\w+) (?P<age>\d+)`)
groups := regextra.NamedGroups(re, "Alice 30")
// groups = map[string]string{"name": "Alice", "age": "30"}
Example
package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
	"regexp"
)

func main() {
	re := regexp.MustCompile(`(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})`)
	groups := rx.NamedGroups(re, "Date: 2025-10-04")

	// Note: map iteration order is not guaranteed, so we print sorted
	keys := []string{"year", "month", "day"}
	for _, key := range keys {
		fmt.Printf("%s=%s ", key, groups[key])
	}
}
Output:
year=2025 month=10 day=04

func NamedGroupsPerMatch

func NamedGroupsPerMatch(re *regexp.Regexp, target string) []map[string]string

NamedGroupsPerMatch returns one map of named-group values per match of re in target, in match order — the every-match counterpart to NamedGroups. Each map follows the same per-match semantics as NamedGroups: every declared group is present, a group that did not participate in that match is mapped to "", and when the pattern reuses a group name the last participating occurrence in that match wins.

On no match, returns an empty (non-nil) slice. See the package doc's "No-match behavior" section for the full cross-API contract. For the lazy, allocation-light streaming form, use NamedGroupsPerMatchSeq. For the typed equivalent that decodes each match into a struct, use UnmarshalAll / Decoder.All.

Example:

re := regexp.MustCompile(`(?P<key>\w+)=(?P<value>\w+)`)
all := regextra.NamedGroupsPerMatch(re, "a=1 b=2")
// all = []map[string]string{{"key": "a", "value": "1"}, {"key": "b", "value": "2"}}
Example
package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
	"regexp"
)

func main() {
	re := regexp.MustCompile(`(?P<key>\w+)=(?P<value>\w+)`)
	all := rx.NamedGroupsPerMatch(re, "a=1 b=2")
	for _, m := range all {
		fmt.Printf("%s=%s\n", m["key"], m["value"])
	}
}
Output:
a=1
b=2

func NamedGroupsPerMatchSeq

func NamedGroupsPerMatchSeq(re *regexp.Regexp, target string) iter.Seq[map[string]string]

NamedGroupsPerMatchSeq is the range-over-func (Go 1.23+) form of NamedGroupsPerMatch: it yields one named-group map per match of re in target, in match order, without building the intermediate slice. Each yielded map follows the same per-match semantics as NamedGroups. Stopping the range early stops the iteration.

On no match, the iterator yields zero times. See the package doc's "No-match behavior" section for the full cross-API contract. For the typed streaming equivalent, use Decoder.Iter.

Example:

re := regexp.MustCompile(`(?P<key>\w+)=(?P<value>\w+)`)
for m := range regextra.NamedGroupsPerMatchSeq(re, "a=1 b=2") {
    fmt.Println(m["key"], m["value"])
}
// Output:
// a 1
// b 2
Example
package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
	"regexp"
)

func main() {
	re := regexp.MustCompile(`(?P<key>\w+)=(?P<value>\w+)`)
	for m := range rx.NamedGroupsPerMatchSeq(re, "a=1 b=2") {
		fmt.Printf("%s=%s\n", m["key"], m["value"])
	}
}
Output:
a=1
b=2

func Replace

func Replace(re *regexp.Regexp, target string, replacements map[string]string) string

Replace substitutes the matched span of each named capture group in target with the value from replacements, leaving non-matching text and any groups absent from the map unchanged. Replace operates on every match of re, in order.

If a regex declares a named group but replacements has no entry for it, the original matched text passes through. Groups that don't participate in a match (optional groups returning index -1) are skipped.

When named groups overlap (nesting), the outermost-named group whose span is encountered first wins; inner groups inside an already-replaced span are not substituted.

On no match, returns target unchanged. See the package doc's "No-match behavior" section for the full cross-API contract.

Example:

re := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>[\w.]+)`)
out := regextra.Replace(re, "alice@example.com", map[string]string{
    "domain": "redacted",
})
// out = "alice@redacted"
Example
package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
	"regexp"
)

func main() {
	re := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>[\w.]+)`)
	out := rx.Replace(re, "alice@example.com bob@other.org", map[string]string{
		"domain": "redacted",
	})
	fmt.Println(out)
}
Output:
alice@redacted bob@redacted

func ReplaceFirst

func ReplaceFirst(re *regexp.Regexp, target string, replacements map[string]string) string

ReplaceFirst is like Replace but substitutes named-group spans only within the first match of re in target; every later match, and all text outside the first match, passes through byte-for-byte unchanged. Use it when only the leading occurrence should be rewritten — e.g. redacting the first token on a line while leaving the rest intact.

Within that first match it follows Replace's rules exactly: a group absent from replacements passes through, non-participating groups are skipped, and on overlap the outermost span encountered first wins.

On no match, returns target unchanged. See the package doc's "No-match behavior" section for the full cross-API contract.

Example:

re := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>[\w.]+)`)
out := regextra.ReplaceFirst(re, "alice@example.com bob@other.org", map[string]string{
    "domain": "redacted",
})
// out = "alice@redacted bob@other.org"
Example
package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
	"regexp"
)

func main() {
	re := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>[\w.]+)`)
	out := rx.ReplaceFirst(re, "alice@example.com bob@other.org", map[string]string{
		"domain": "redacted",
	})
	fmt.Println(out)
}
Output:
alice@redacted bob@other.org

func ReplaceFunc

func ReplaceFunc(re *regexp.Regexp, target string, fn func(group, match string) string) string

ReplaceFunc substitutes the matched span of each named capture group in target with the string returned by fn, which is called with the group's name and the text it matched. Like Replace it operates on every match of re, in order — but the replacement is computed from the matched value rather than looked up in a static map. Use it for substitutions that depend on what matched: redaction (mask all but the last four digits of a card number), normalization (lowercase a captured host), and similar.

fn runs once per substituted named span, left to right. To leave a group unchanged, return its match verbatim. This differs from Replace, which passes a group through when it is absent from the map: every participating named group reaches fn, so fn itself decides what to keep. Passing a nil fn is a programmer error — it panics on the first match, mirroring the standard library's regexp.Regexp.ReplaceAllStringFunc.

When named groups overlap (nesting), the outermost named group whose span is encountered first wins; fn is not called for inner groups inside an already-substituted span — matching Replace's overlap rule.

On no match, returns target unchanged and never calls fn. See the package doc's "No-match behavior" section for the full cross-API contract.

Example — mask all but the last four digits of a captured card number:

re := regexp.MustCompile(`(?P<card>\d{12,19})`)
out := regextra.ReplaceFunc(re, "card 4111111111111111 ok", func(group, match string) string {
    return strings.Repeat("*", len(match)-4) + match[len(match)-4:]
})
// out = "card ************1111 ok"
Example
package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
	"regexp"
	"strings"
)

func main() {
	// Mask all but the last four digits of a captured card number.
	re := regexp.MustCompile(`(?P<card>\d{12,19})`)
	out := rx.ReplaceFunc(re, "card 4111111111111111 ok", func(_, match string) string {
		return strings.Repeat("*", len(match)-4) + match[len(match)-4:]
	})
	fmt.Println(out)
}
Output:
card ************1111 ok

func Unmarshal

func Unmarshal(re *regexp.Regexp, target string, v any) error

Unmarshal extracts named capture groups from the target string and assigns them to the corresponding fields in the provided struct pointer.

Field mapping rules:

  • First checks the `regex:"groupname"` struct tag if provided (highest priority)
  • Falls back to exact field name match with capture group name
  • Falls back to case-insensitive field name match (Unicode simple case folding)
  • Supports type conversion for string, bool, all int/uint widths, float32/float64, time.Time, time.Duration, and pointers (any depth) to any of these; types implementing RegexUnmarshaler or encoding.TextUnmarshaler convert themselves
  • Unexported fields are ignored
  • A group that did not participate in the match (e.g. an optional group), or that matched an empty span, leaves the field unchanged — unless the field has a `default=` tag option, which substitutes instead

On no match, Unmarshal returns nil and leaves *v unchanged — no match is data absence, not a failure. Callers who need to distinguish "matched" from "didn't match" should inspect their struct after the call, or use Decoder.One which signals no-match via ErrNoMatch. See the package doc's "No-match behavior" section for the full cross-API contract.

Returns an error if:

  • v is not a pointer to a struct
  • Type conversion fails on a matched group (a DecodeError)
  • A `required` field's group produced no value (a RequiredGroupError): it did not participate in the match or matched an empty span, and no `default=` supplied a substitute
  • A matched field is a nested struct, slice, or map: these are not flattened, so a group bound to one yields an "unsupported field type" error (unless the type implements RegexUnmarshaler or encoding.TextUnmarshaler, which convert themselves)

Example:

type Person struct {
    Name string
    Age  int `regex:"age"`
}
re := regexp.MustCompile(`(?P<name>\w+) is (?P<age>\d+)`)
var person Person
err := regextra.Unmarshal(re, "Alice is 30", &person)
// person.Name = "Alice", person.Age = 30
Example
package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
	"regexp"
)

func main() {
	type Person struct {
		Name string
		Age  int
	}

	re := regexp.MustCompile(`(?P<name>\w+) is (?P<age>\d+) years old`)
	var person Person
	err := rx.Unmarshal(re, "Alice is 30 years old", &person)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Printf("%s is %d\n", person.Name, person.Age)
}
Output:
Alice is 30
Example (DefaultAndLayout)
package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
	"regexp"
	"time"
)

func main() {
	type LogLine struct {
		TS    time.Time `regex:"ts,layout=02/Jan/2006:15:04:05 -0700"`
		Level string    `regex:"level,default=info"`
	}
	re := regexp.MustCompile(`\[(?P<ts>[^\]]+)\]\s*(?P<other>.*)`)
	var l LogLine
	_ = rx.Unmarshal(re, "[26/Apr/2026:12:34:56 -0500] something happened", &l)
	fmt.Printf("%s @ %s\n", l.Level, l.TS.Format("2006-01-02"))
}
Output:
info @ 2026-04-26
Example (PointerFields)
package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
	"regexp"
)

func main() {
	type Result struct {
		Name *string `regex:"name"`
		Age  *int    `regex:"age"`
	}
	re := regexp.MustCompile(`(?P<name>\w+)\s+is\s+(?P<age>\d+)`)
	var r Result
	_ = rx.Unmarshal(re, "Alice is 30", &r)
	fmt.Printf("%s, %d\n", *r.Name, *r.Age)
}
Output:
Alice, 30
Example (StructTags)
package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
	"regexp"
)

func main() {
	type Email struct {
		Username string `regex:"user"`
		Domain   string `regex:"domain"`
	}

	re := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>[\w.]+)`)
	var email Email
	err := rx.Unmarshal(re, "alice@example.com", &email)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Printf("User: %s, Domain: %s\n", email.Username, email.Domain)
}
Output:
User: alice, Domain: example.com
Example (TextUnmarshaler)

ExampleUnmarshal_textUnmarshaler shows that any field type implementing encoding.TextUnmarshaler (here the stdlib's netip.Addr) is populated from its capture group via UnmarshalText, with no regextra-specific code required.

package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
	"net/netip"
	"regexp"
)

func main() {
	type Conn struct {
		Addr netip.Addr `regex:"addr"`
	}
	re := regexp.MustCompile(`from (?P<addr>\S+)`)
	var c Conn
	_ = rx.Unmarshal(re, "from 192.168.0.1", &c)
	fmt.Println(c.Addr)
}
Output:
192.168.0.1
Example (TimeTypes)
package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
	"regexp"
	"time"
)

func main() {
	type Event struct {
		Started time.Time     `regex:"start"`
		Took    time.Duration `regex:"took"`
	}
	re := regexp.MustCompile(`(?P<start>\S+)\s+\((?P<took>\S+)\)`)
	var ev Event
	_ = rx.Unmarshal(re, "2026-04-26T12:34:56Z (1h30m)", &ev)
	fmt.Printf("%s for %s\n", ev.Started.Format(time.RFC3339), ev.Took)
}
Output:
2026-04-26T12:34:56Z for 1h30m0s

func UnmarshalAll

func UnmarshalAll(re *regexp.Regexp, target string, v any) error

UnmarshalAll extracts all occurrences of the regex pattern from the target string and unmarshals them into a slice of structs. The slice is cleared before populating.

v must be a pointer to a slice of structs. On no matches, UnmarshalAll returns nil and sets the slice length to 0 — no match is data absence, not a failure. See the package doc's "No-match behavior" section for the full cross-API contract.

Returns an error if v is not a pointer to a slice of structs, or if any match fails to decode — a per-field conversion failure (a DecodeError) or a `required` group that produced no value (a RequiredGroupError). The first failing match stops the call and returns that error, prefixed with its match index.

Example:

type Person struct {
    Name string
    Age  int
}
re := regexp.MustCompile(`(?P<name>\w+) is (?P<age>\d+)`)
var people []Person
err := regextra.UnmarshalAll(re, "Alice is 30 and Bob is 25", &people)
// people = []Person{{Name: "Alice", Age: 30}, {Name: "Bob", Age: 25}}
Example
package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
	"regexp"
)

func main() {
	type Person struct {
		Name string
		Age  int
	}

	re := regexp.MustCompile(`(?P<name>\w+) is (?P<age>\d+)`)
	var people []Person
	err := rx.UnmarshalAll(re, "Alice is 30 and Bob is 25", &people)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	for _, person := range people {
		fmt.Printf("%s: %d\n", person.Name, person.Age)
	}
}
Output:
Alice: 30
Bob: 25

func Validate

func Validate(re *regexp.Regexp, required ...string) error

Validate returns an error listing every required group name that is not declared on re. Useful for init-time assertions in services that compile patterns once: catch typos at startup rather than at the first (mis-)matched request.

Returns nil when every required name is declared. On failure it returns an errors.As-able *MissingNamedGroupsError whose Missing field lists the missing names in the order they were passed.

Example:

re := regexp.MustCompile(`(?P<name>\w+) (?P<age>\d+)`)
if err := regextra.Validate(re, "name", "age", "missing"); err != nil {
    // err: regextra.Validate: missing named groups: missing
}
Example

status is a custom type whose pointer satisfies RegexUnmarshaler; used by TestUnmarshalRegexUnmarshaler.

package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
	"regexp"
)

func main() {
	re := regexp.MustCompile(`(?P<name>\w+) (?P<age>\d+)`)
	if err := rx.Validate(re, "name", "age", "ssn"); err != nil {
		fmt.Println(err)
	}
}
Output:
regextra.Validate: missing named groups: ssn

Types

type DecodeError

type DecodeError struct {
	// Field is the destination struct field name.
	Field string
	// Group is the capture group the value was read from: the field's
	// `regex:"..."` tag name when set, otherwise the declared group whose name
	// matches the field name. It is empty only when the field maps to no
	// declared group at all — a default-only field. Unmarshal and Decoder share
	// one decode path, so that empty-Group case differs by strictness, not by
	// API: it is observable only on the lenient [Unmarshal]/[UnmarshalAll] path,
	// where a `default=` value that fails to convert raises a DecodeError at
	// decode time. The strict [Decoder.One]/[Decoder.All]/[Decoder.Iter] path
	// validates such defaults at [Compile], so it never surfaces an empty-Group
	// DecodeError at runtime.
	Group string
	// Value is the raw matched string (or substituted default) that failed to
	// convert.
	Value string
	// Type is the destination field's type, rendered (e.g. "int", "time.Time").
	Type string
	// Err is the underlying conversion error.
	Err error
}

DecodeError reports the failure to convert a matched capture-group value into its destination struct field. It is returned (wrapped with the calling entrypoint's prefix) by Unmarshal, UnmarshalAll, Decoder.One, Decoder.All, and Decoder.Iter when a field's type conversion fails on a participating match. Recover it with errors.As to branch on the failure without parsing message text:

var de *regextra.DecodeError
if errors.As(err, &de) {
    log.Printf("field %s (group %s) could not parse %q as %s", de.Field, de.Group, de.Value, de.Type)
}

Err holds the underlying conversion cause (e.g. a *strconv.NumError or a time-parsing error) and is reachable via errors.Is/errors.As through Unwrap. No match is not a DecodeError — Unmarshal/UnmarshalAll return nil and Decoder.One returns ErrNoMatch in that case.

func (*DecodeError) Error

func (e *DecodeError) Error() string

Error implements the error interface. The calling entrypoint prepends its own `regextra.<Entrypoint>:` prefix when wrapping. When Err is nil (only reachable by constructing the value directly — the decode path always sets an underlying cause) it reports "no decode error" rather than a message with a dangling "<nil>" cause.

func (*DecodeError) Unwrap

func (e *DecodeError) Unwrap() error

Unwrap returns the underlying conversion error so errors.Is/errors.As can reach it.

type Decoder

type Decoder[T any] struct {
	// contains filtered or unexported fields
}

Decoder is a typed, regex-bound unmarshaler that carries the reflect plan for T's fields. Compile once, decode many times — Unmarshal caches its decode plan per (pattern, struct type) pair too, so the Decoder's remaining edge is skipping the per-call cache lookup, plus the strict compile-time validation below.

The decode plan is computed during Compile: each exported field of T is mapped to its regex capture group, its tag options are parsed, and any default value is type-checked. Runtime Decoder.One / Decoder.All calls walk the precomputed plan against the regex match indices, never running reflect on the destination type.

Decoders are safe for concurrent use — no shared mutable state after Compile. Reuse one Decoder per goroutine pool / per request handler.

Use Compile (returns error) or MustCompile (panics) to construct.

func Compile

func Compile[T any](pattern string) (*Decoder[T], error)

Compile parses pattern and validates T's struct tags against it.

Returns an error if:

  • pattern is not a valid regular expression
  • T is not a struct type
  • A field's `regex:"name"` tag references a group not declared on pattern, and the field has no `default=` (a `default=` makes a missing group intentional, so the field compiles and the default always fires)
  • A field's `regex:",default=<value>"` cannot be converted to the field's type
  • A field uses `regex:",layout=..."` on a non-time.Time field

Once Compile returns nil, the resulting Decoder is fully validated and guaranteed not to produce tag-related errors at decode time.

Failures are categorized by wrapped sentinel: the first cause above wraps ErrInvalidPattern and the remaining four wrap ErrInvalidStruct, so callers can branch on the failure kind with errors.Is instead of parsing the message. MustCompile panics with the same wrapped error.

Example
package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
)

func main() {
	type Person struct {
		Name string `regex:"name"`
		Age  int    `regex:"age"`
	}
	dec, err := rx.Compile[Person](`(?P<name>\w+) is (?P<age>\d+)`)
	if err != nil {
		fmt.Println(err)
		return
	}
	p, err := dec.One("Alice is 30")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Printf("%s, %d\n", p.Name, p.Age)
}
Output:
Alice, 30

func MustCompile

func MustCompile[T any](pattern string) *Decoder[T]

MustCompile is like Compile but panics on error. Intended for package-level vars where startup-time failure is the right behavior:

var personDecoder = regextra.MustCompile[Person](`(?P<name>\w+) is (?P<age>\d+)`)
Example
package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
)

func main() {
	type Person struct {
		Name string `regex:"name"`
	}
	// Package-level vars commonly use MustCompile so a typo fails the build.
	var dec = rx.MustCompile[Person](`(?P<name>\w+)`)
	p, _ := dec.One("Alice")
	fmt.Println(p.Name)
}
Output:
Alice

func (*Decoder[T]) All

func (d *Decoder[T]) All(target string) ([]T, error)

All returns every match of d's pattern in target decoded into a slice. Returns an empty slice and nil error when there are no matches. A non-nil error indicates a per-field conversion failure (a DecodeError) or a `required` group that produced no value (a RequiredGroupError) on one of the matches; the slice up to that point may contain partially-decoded entries.

Example
package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
)

func main() {
	type Entry struct {
		Name string `regex:"name"`
		Age  int    `regex:"age"`
	}
	dec := rx.MustCompile[Entry](`(?P<name>\w+) is (?P<age>\d+)`)
	people, _ := dec.All("Alice is 30 and Bob is 25")
	for _, p := range people {
		fmt.Printf("%s/%d\n", p.Name, p.Age)
	}
}
Output:
Alice/30
Bob/25

func (*Decoder[T]) Encoder

func (d *Decoder[T]) Encoder() (*Encoder[T], error)

Encoder derives the typed inverse of d by inverting d's compiled pattern: it parses the pattern's AST and walks the invertible subset (literal runs, named capture groups, anchors, pure-literal unnamed groups) into an ordered encode plan. Named capture groups resolve to struct fields with the same field-mapping rules Decoder uses. Write the pattern once and get the encoder for free — there is no separate template to keep in sync.

Returns an error if:

  • the pattern contains a construct that is not invertible outside a named capture group — an alternation (`|`), a quantifier (`*`, `+`, `?`, `{n,m}`), a character class (`[...]`), an any-character wildcard (`.`), or an unnamed group with non-literal content — wrapping ErrNotInvertible
  • a named capture group maps to no exported, non-excluded field of T
  • a mapped field's type cannot be encoded (see Encoder for the supported set)

The latter two wrap ErrInvalidStruct, mirroring Compile. Once Encoder returns nil, the resulting Encoder is fully validated: the only errors Encoder.Encode can then surface are runtime value failures (a custom marshaler returning an error, or a nil pointer or nil interface field).

Example
package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
)

func main() {
	type Person struct {
		Name string `regex:"name"`
		Age  int    `regex:"age"`
	}
	dec := rx.MustCompile[Person](`(?P<name>\S+) is (?P<age>\d+)`)
	enc, _ := dec.Encoder()
	s, _ := enc.Encode(Person{Name: "Alice", Age: 30})
	fmt.Println(s)
}
Output:
Alice is 30
Example (RoundTrip)
package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
)

func main() {
	type Person struct {
		Name string `regex:"name"`
		Age  int    `regex:"age"`
	}
	dec := rx.MustCompile[Person](`(?P<name>\S+) is (?P<age>\d+)`)
	enc, _ := dec.Encoder()

	s, _ := enc.Encode(Person{Name: "Alice", Age: 30})
	back, _ := dec.One(s)
	fmt.Printf("%q -> %+v\n", s, back)
}
Output:
"Alice is 30" -> {Name:Alice Age:30}

func (*Decoder[T]) Iter

func (d *Decoder[T]) Iter(target string) iter.Seq2[T, error]

Iter returns a range-over-func iterator that decodes each match of d's pattern in target into a T, yielded with the per-match decode error. nil error means decoded successfully; non-nil means a per-field conversion failed (a DecodeError) or a `required` group produced no value (a RequiredGroupError) on that match. Iteration continues past errors so callers can collect or skip individual failures:

for v, err := range dec.Iter(input) {
    if err != nil {
        log.Printf("skipping bad match: %v", err)
        continue
    }
    process(v)
}

Break out of the range body to stop iteration early (e.g. after the first match). The match-finding step is not lazy — Go's regexp package pre-computes all match positions in one call — but the decode step IS lazy, so breaking early avoids the per-match reflect work for the remaining matches.

For a slice of all results with a single error, prefer Decoder.All. For a single match with a sentinel ErrNoMatch, prefer Decoder.One.

Example
package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
)

func main() {
	type Entry struct {
		Name string `regex:"name"`
		Age  int    `regex:"age"`
	}
	dec := rx.MustCompile[Entry](`(?P<name>\w+) is (?P<age>\d+)`)
	for p, err := range dec.Iter("Alice is 30 and Bob is 25") {
		if err != nil {
			continue
		}
		fmt.Printf("%s/%d\n", p.Name, p.Age)
	}
}
Output:
Alice/30
Bob/25

func (*Decoder[T]) One

func (d *Decoder[T]) One(target string) (T, error)

One returns the result of decoding the first match of d's pattern in target. Returns ErrNoMatch if there's no match. Other errors indicate either a per-field conversion failure (a DecodeError) or a `required` group that produced no value (a RequiredGroupError); in that case the returned T contains whatever fields were successfully decoded before the failure. A matched field whose type is a nested struct, slice, or map is one such failure: these are not flattened, so binding a group to one yields an "unsupported field type" error (unless the type implements RegexUnmarshaler or encoding.TextUnmarshaler, which convert themselves). The same applies to Decoder.All and Decoder.Iter, which share One's decode path.

One uses the ErrNoMatch sentinel because the (T, error) return shape would otherwise make "no match" indistinguishable from "decoded a struct of all zero fields". Compare with errors.Is. See the package doc's "No-match behavior" section for the full cross-API contract.

func (*Decoder[T]) Pattern

func (d *Decoder[T]) Pattern() string

Pattern returns the regex source pattern this Decoder was compiled from. Useful for logging and debugging.

Example
package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
)

func main() {
	type Entry struct {
		Name string `regex:"name"`
		Age  int    `regex:"age"`
	}
	dec := rx.MustCompile[Entry](`(?P<name>\w+) is (?P<age>\d+)`)
	// Pattern returns the regex source the Decoder was compiled from.
	fmt.Println(dec.Pattern())
}
Output:
(?P<name>\w+) is (?P<age>\d+)

func (*Decoder[T]) Regexp

func (d *Decoder[T]) Regexp() *regexp.Regexp

Regexp returns the compiled *regexp.Regexp this Decoder built from its pattern, so callers can reuse it for their own match-finding (for example regexp.Regexp.FindAllIndex or custom iteration) without recompiling.

The returned pointer is shared with the Decoder. Its exported methods are read-only and safe for concurrent use, but callers must not mutate shared state on it — in particular do not call regexp.Regexp.Longest, which changes the receiver's matching semantics and would affect the Decoder too.

Example
package main

import (
	"fmt"

	rx "github.com/jecoms/regextra/v2"
)

func main() {
	type Entry struct {
		Name string `regex:"name"`
		Age  int    `regex:"age"`
	}
	dec := rx.MustCompile[Entry](`(?P<name>\w+) is (?P<age>\d+)`)
	// Regexp exposes the compiled *regexp.Regexp for your own match-finding,
	// without recompiling the pattern. Treat it as read-only.
	for _, m := range dec.Regexp().FindAllString("Alice is 30 and Bob is 25", -1) {
		fmt.Println(m)
	}
}
Output:
Alice is 30
Bob is 25

type EncodeError

type EncodeError struct {
	// Field is the source struct field name.
	Field string
	// Group is the capture-group name the field resolved from: the field's
	// `regex:"..."` tag name when set, otherwise the declared group whose name
	// matches the field name — which may differ from the field name in case when
	// the two matched via Unicode simple case folding. Mirrors [DecodeError].Group.
	Group string
	// Type is the source field's type, rendered (e.g. "int", "time.Time").
	Type string
	// Err is the underlying encode error.
	Err error
}

EncodeError reports the failure to render a struct field into its capture-group slot. It is the encode-side mirror of DecodeError, returned (wrapped with the calling entrypoint's prefix) by Encoder.Encode when a field's value cannot be converted to a string. Recover it with errors.As to branch on the failure without parsing message text:

var ee *regextra.EncodeError
if errors.As(err, &ee) {
    log.Printf("field %s (group %s) of type %s could not encode: %v", ee.Field, ee.Group, ee.Type, ee.Err)
}

Err holds the underlying cause (e.g. an error from a custom RegexMarshaler or encoding.TextMarshaler, or a nil-pointer or nil-interface field) and is reachable via errors.Is/errors.As through Unwrap.

func (*EncodeError) Error

func (e *EncodeError) Error() string

Error implements the error interface. The calling entrypoint prepends its own `regextra.<Entrypoint>:` prefix when wrapping. When Err is nil (only reachable by constructing the value directly — the encode path always sets an underlying cause) it reports "no encode error" rather than a message with a dangling "<nil>" cause, mirroring DecodeError, RequiredGroupError, and MissingNamedGroupsError.

func (*EncodeError) Unwrap

func (e *EncodeError) Unwrap() error

Unwrap returns the underlying encode error so errors.Is/errors.As can reach it.

type Encoder

type Encoder[T any] struct {
	// contains filtered or unexported fields
}

Encoder is the typed inverse of Decoder: it renders a value of T back into a string so that an Encode followed by an Unmarshal / Decoder.One on the same pattern round-trips the original struct. Construct one with Decoder.Encoder, which derives the encoder from the decoder's own compiled pattern — write the pattern once, get the inverse for free.

Derivation

Decoder.Encoder parses the decoder's pattern with regexp/syntax and inverts the invertible subset of the grammar into an ordered plan of literal runs and field substitutions:

  • Literal text is emitted verbatim (regexp escapes like `\.` are already decoded by the parser).
  • A named capture group `(?P<name>…)` becomes a field substitution: name resolves to a struct field with the same rules Decoder uses — the field's `regex:"name"` tag matched exactly, otherwise the field's own name matched exactly then case-insensitively via Unicode simple case folding; a `regex:"-"` field is excluded. The group's sub-pattern is discarded — the field's value fills the span.
  • Anchors and zero-width assertions (`^`, `$`, `\A`, `\z`, `\b`, …) match no text and are dropped.
  • An unnamed group whose body is pure literal text is treated as that literal.

Any construct with no single string to emit — an alternation, a quantifier, a character class, an any-character wildcard, or an unnamed group with non-literal content — appearing outside a named capture group makes the pattern non-invertible, and Decoder.Encoder fails fast with ErrNotInvertible.

Decoder.Encoder builds the plan once; Encoder.Encode walks it and concatenates with a strings.Builder. It still reflects on the value each call to read fields, but does no per-call field-mapping reflection — the field↦group resolution is cached in the plan at construction.

Supported field types

A named group resolves to a field whose type Encode can render — the same set Unmarshal accepts on the decode side: string, bool, all int/uint widths, float32/float64, time.Time, time.Duration, a type implementing RegexMarshaler or encoding.TextMarshaler, and pointers (any depth) to any of these. A mapped field of any other type makes Decoder.Encoder fail with ErrInvalidStruct. A time.Time field honors a `layout=` tag option and otherwise emits RFC3339Nano.

Round-trip contract

Encode(v) re-decodes to v when each encoded value re-matches the sub-pattern of the group it fills. The caller owns that pairing by writing value-appropriate sub-patterns in the decode regex (a captured word wants `\S+`, not `.*`). Values that collide with a surrounding literal delimiter, or two adjacent captures with no literal between them, have no unambiguous decode boundary and are out of scope. (A future option is to re-match each encoded value against its group's sub-pattern at Encode time; that is deliberately not done here.)

Encoders are safe for concurrent use — no shared mutable state after construction.

func (*Encoder[T]) Encode

func (e *Encoder[T]) Encode(v T) (string, error)

Encode renders v into a string by walking e's derived plan: literal segments pass through and each named-group slot is replaced with the encoded value of its struct field.

Returns an EncodeError (wrapped with the entrypoint prefix) if a field cannot be rendered at runtime — a custom RegexMarshaler / encoding.TextMarshaler returning an error, or a nil pointer or nil interface field, which has no string form.

The `default=` tag option does not affect encoding: it is a decode-side substitution for an absent group, whereas Encode always emits the field's actual value. `layout=` is honored so a time.Time re-parses under Decoder's exclusive-layout rule.

type MissingNamedGroupsError

type MissingNamedGroupsError struct {
	// Missing holds the declared-but-absent required group names, in the order
	// they were passed to Validate.
	Missing []string
}

MissingNamedGroupsError reports the required group names that Validate could not find declared on the pattern. It is returned (wrapped with the `regextra.Validate:` prefix) whenever at least one required name is missing. Recover it with errors.As to branch on the missing set without parsing message text:

var ve *regextra.MissingNamedGroupsError
if errors.As(err, &ve) {
    log.Printf("pattern is missing groups: %v", ve.Missing)
}

Missing lists the absent names in the order they were passed to Validate. Unlike DecodeError there is no underlying cause to unwrap — Missing is the payload.

func (*MissingNamedGroupsError) Error

func (e *MissingNamedGroupsError) Error() string

Error implements the error interface. Validate prepends its own `regextra.Validate:` prefix when wrapping. When Missing is empty (only reachable by constructing the value directly — Validate never wraps an empty set) it reports "no missing named groups" rather than a message with a dangling separator.

type RegexMarshaler

type RegexMarshaler interface {
	MarshalRegex() (string, error)
}

RegexMarshaler is the interface implemented by types that render themselves into a string for the regextra encode path. It is the encode-side mirror of RegexUnmarshaler and of encoding.TextMarshaler: when an Encoder field's type satisfies this interface, Encoder.Encode calls MarshalRegex instead of the built-in string/int/uint/float/bool conversion.

A type that implements both RegexMarshaler and RegexUnmarshaler round-trips symmetrically through Encoder and Decoder.

Example:

type Status int

func (s Status) MarshalRegex() (string, error) {
    switch s {
    case StatusOpen:   return "open", nil
    case StatusClosed: return "closed", nil
    default:           return "", fmt.Errorf("unknown status: %d", s)
    }
}
Example
package main

import (
	"fmt"
)

func main() {
	type Severity int
	const (
		_ Severity = iota
		Low
		Medium
		High
	)
	// In real code this would be a type defined in the same package as
	// the call to Encode, with `func (s Severity) MarshalRegex() (string, error)`.
	// Compile-time check elided here for example brevity.
	_ = Low
	_ = Medium
	_ = High
	fmt.Println("see TestEncode_regexMarshaler for a runnable demo")
}
Output:
see TestEncode_regexMarshaler for a runnable demo

type RegexUnmarshaler

type RegexUnmarshaler interface {
	UnmarshalRegex(value string) error
}

RegexUnmarshaler is the interface implemented by types that know how to initialize themselves from a regex group's matched string. It mirrors encoding.TextUnmarshaler for the regextra unmarshal path: when a destination field's pointer type satisfies this interface, Unmarshal (and UnmarshalAll) call UnmarshalRegex with the matched group value instead of running the built-in string/int/uint/float/bool conversion.

This is the extension point for caller-defined types that the built-in type switch can't handle (URLs, enums, big numbers, IP addresses, etc.).

Example:

type Status int

func (s *Status) UnmarshalRegex(value string) error {
    switch value {
    case "open":   *s = StatusOpen
    case "closed": *s = StatusClosed
    default:       return fmt.Errorf("unknown status: %q", value)
    }
    return nil
}
Example
package main

import (
	"fmt"
)

func main() {
	type Severity int
	const (
		_ Severity = iota
		Low
		Medium
		High
	)
	// In real code this would be a type defined in the same package as
	// the call to Unmarshal, with `func (s *Severity) UnmarshalRegex(...) error`.
	// Compile-time check elided here for example brevity.
	_ = Low
	_ = Medium
	_ = High
	fmt.Println("see TestUnmarshalRegexUnmarshaler for a runnable demo")
}
Output:
see TestUnmarshalRegexUnmarshaler for a runnable demo

type RequiredGroupError

type RequiredGroupError struct {
	// Field is the destination struct field name.
	Field string
	// Group is the capture group the required value was expected from: the
	// field's `regex:"..."` tag name when set, otherwise the declared group
	// whose name matches the field name. It is empty only when a `required`
	// field maps to no declared group at all.
	Group string
}

RequiredGroupError reports that a field marked `regex:",required"` produced no value for a match: its capture group did not participate, or matched an empty span, and no `default=` supplied a substitute. It is returned (wrapped with the calling entrypoint's prefix) by Unmarshal, UnmarshalAll, Decoder.One, Decoder.All, and Decoder.Iter. Recover it with errors.As to branch on the missing field without parsing message text:

var rge *regextra.RequiredGroupError
if errors.As(err, &rge) {
    log.Printf("field %s (group %s) is required but had no value", rge.Field, rge.Group)
}

It complements DecodeError (a participating value that failed type conversion) and MissingNamedGroupsError (a static Validate check that the pattern declares a group at all): RequiredGroupError is the per-match presence check. Like MissingNamedGroupsError, there is no underlying cause to unwrap — the absent value is the payload.

func (*RequiredGroupError) Error

func (e *RequiredGroupError) Error() string

Error implements the error interface. The calling entrypoint prepends its own `regextra.<Entrypoint>:` prefix when wrapping. When Field is empty (only reachable by constructing the value directly — the decode path always sets the field name) it reports "no required group error" rather than a message about a nameless field.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL