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:
- Pull one named group from one match: FindNamed
- Pull one named group across all matches: FindAllNamed
- Pull every named group from one match (map): NamedGroups
- Pull every named group from one match, keeping every value when a group name is reused inside the pattern (map of slices): AllNamedGroups
- Pull every named group across all matches (one map per match): NamedGroupsPerMatch, or lazily NamedGroupsPerMatchSeq
- Substitute named-group spans by name: Replace
- Substitute named-group spans in the first match only: ReplaceFirst
- Substitute named-group spans with a callback over the matched value: ReplaceFunc
- Assert at startup that required groups are declared: Validate
- Decode one match into a struct: Unmarshal
- Decode all matches into a slice of structs: UnmarshalAll
- Decode the same shape repeatedly with cached reflect work: Compile, MustCompile, Decoder
- Stream matches lazily (Go 1.23+ range-over-func): Decoder.Iter
- Render a struct back into a string by inverting the decoder's own compiled pattern (the typed inverse of Decoder): Decoder.Encoder, Encoder
- Plug in caller-defined types in the unmarshal path: RegexUnmarshaler
- Plug in caller-defined types in the encode path: RegexMarshaler
- Compare against the no-match sentinel: ErrNoMatch
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 ¶
- Variables
- func AllNamedGroups(re *regexp.Regexp, target string) map[string][]string
- func FindAllNamed(re *regexp.Regexp, target, groupName string) []string
- func FindNamed(re *regexp.Regexp, target, groupName string) (string, bool)
- func NamedGroups(re *regexp.Regexp, target string) map[string]string
- func NamedGroupsPerMatch(re *regexp.Regexp, target string) []map[string]string
- func NamedGroupsPerMatchSeq(re *regexp.Regexp, target string) iter.Seq[map[string]string]
- func Replace(re *regexp.Regexp, target string, replacements map[string]string) string
- func ReplaceFirst(re *regexp.Regexp, target string, replacements map[string]string) string
- func ReplaceFunc(re *regexp.Regexp, target string, fn func(group, match string) string) string
- func Unmarshal(re *regexp.Regexp, target string, v any) error
- func UnmarshalAll(re *regexp.Regexp, target string, v any) error
- func Validate(re *regexp.Regexp, required ...string) error
- type DecodeError
- type Decoder
- type EncodeError
- type Encoder
- type MissingNamedGroupsError
- type RegexMarshaler
- type RegexUnmarshaler
- type RequiredGroupError
Examples ¶
- AllNamedGroups
- Compile
- Decoder.All
- Decoder.Encoder
- Decoder.Encoder (RoundTrip)
- Decoder.Iter
- Decoder.Pattern
- Decoder.Regexp
- FindAllNamed
- FindNamed
- MustCompile
- NamedGroups
- NamedGroupsPerMatch
- NamedGroupsPerMatchSeq
- RegexMarshaler
- RegexUnmarshaler
- Replace
- ReplaceFirst
- ReplaceFunc
- Unmarshal
- Unmarshal (DefaultAndLayout)
- Unmarshal (PointerFields)
- Unmarshal (StructTags)
- Unmarshal (TextUnmarshaler)
- Unmarshal (TimeTypes)
- UnmarshalAll
- Validate
Constants ¶
This section is empty.
Variables ¶
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.
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
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 ¶
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 ¶
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.