Skip to content

Commit 1c5f812

Browse files
authored
Merge pull request #792 from costela/add-support-for-dualstack-ip-parsing
proposal: extend netip.Addr support to ipv6
2 parents ce59480 + 20d3402 commit 1c5f812

6 files changed

Lines changed: 44 additions & 12 deletions

File tree

docs/docs/features/request-inputs.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ The following special types are supported out of the box:
107107
| `time.Time` | `{"type": "string", "format": "date-time"}` | `"2020-01-01T12:00:00Z"` |
108108
| `url.URL` | `{"type": "string", "format": "uri"}` | `"https://example.com"` |
109109
| `net.IP` | `{"type": "string", "format": "ipv4"}` | `"127.0.0.1"` |
110-
| `netip.Addr` | `{"type": "string", "format": "ipv4"}` | `"127.0.0.1"` |
110+
| `netip.Addr` | `{"type": "string", "format": "ip"}` | `"127.0.0.1"` or `fe80::1` |
111111
| `json.RawMessage` | `{}` | `["whatever", "you", "want"]` |
112112

113113
You can override this default behavior if needed as described in [Schema Customization](./schema-customization.md) and [Request Validation](./request-validation.md), e.g. setting a custom `format` tag for IPv6.

schema.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -750,7 +750,7 @@ func schemaFromType(r Registry, t reflect.Type) *Schema {
750750
case ipType:
751751
return &Schema{Type: TypeString, Nullable: isPointer, Format: "ipv4"}
752752
case ipAddrType:
753-
return &Schema{Type: TypeString, Nullable: isPointer, Format: "ipv4"}
753+
return &Schema{Type: TypeString, Nullable: isPointer, Format: "ip"}
754754
case rawMessageType:
755755
return &Schema{}
756756
}

schema_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ func TestSchema(t *testing.T) {
190190
{
191191
name: "ipAddr",
192192
input: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
193-
expected: `{"type": "string", "format": "ipv4"}`,
193+
expected: `{"type": "string", "format": "ip"}`,
194194
},
195195
{
196196
name: "json.RawMessage",

validate.go

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import (
44
"errors"
55
"fmt"
66
"math"
7-
"net"
87
"net/mail"
8+
"net/netip"
99
"net/url"
1010
"reflect"
1111
"regexp"
@@ -249,18 +249,27 @@ func validateFormat(path *PathBuffer, str string, s *Schema, res *ValidateResult
249249
if len(str) >= 256 || !rxHostname.MatchString(str) {
250250
res.Add(path, str, validation.MsgExpectedRFC5890Hostname)
251251
}
252+
case "ipv4", "ipv6", "ip":
253+
addr, err := netip.ParseAddr(str)
254+
255+
switch s.Format {
256+
case "ipv4":
257+
if err != nil || !addr.Is4() {
258+
res.Add(path, str, validation.MsgExpectedRFC2673IPv4)
259+
}
260+
case "ipv6":
261+
if err != nil || !addr.Is6() || addr.Is4In6() {
262+
res.Add(path, str, validation.MsgExpectedRFC2373IPv6)
263+
}
264+
default: // case "ip".
265+
if err != nil {
266+
res.Add(path, str, validation.MsgExpectedRFCIPAddr)
267+
}
268+
}
252269
case "idn-hostname":
253270
if _, err := idnaProfile.ToASCII(str); err != nil {
254271
res.Add(path, str, validation.MsgExpectedRFC5890Hostname)
255272
}
256-
case "ipv4":
257-
if ip := net.ParseIP(str); ip == nil || ip.To4() == nil {
258-
res.Add(path, str, validation.MsgExpectedRFC2673IPv4)
259-
}
260-
case "ipv6":
261-
if ip := net.ParseIP(str); ip == nil || ip.To16() == nil {
262-
res.Add(path, str, validation.MsgExpectedRFC2373IPv6)
263-
}
264273
case "uri", "uri-reference", "iri", "iri-reference":
265274
if _, err := url.Parse(str); err != nil {
266275
res.Add(path, str, ErrorFormatter(validation.MsgExpectedRFC3986URI, err))

validate_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,28 @@ var validateTests = []struct {
456456
input: map[string]any{"value": "1234"},
457457
errs: []string{"expected string to be RFC 2373 ipv6"},
458458
},
459+
{
460+
name: "ipv4 success",
461+
typ: reflect.TypeOf(struct {
462+
Value string `json:"value" format:"ip"`
463+
}{}),
464+
input: map[string]any{"value": "127.0.0.1"},
465+
},
466+
{
467+
name: "ipv6 success",
468+
typ: reflect.TypeOf(struct {
469+
Value string `json:"value" format:"ip"`
470+
}{}),
471+
input: map[string]any{"value": "2001:0db8:85a3:0000:0000:8a2e:0370:7334"},
472+
},
473+
{
474+
name: "expected ipv4 or ipv6",
475+
typ: reflect.TypeOf(struct {
476+
Value string `json:"value" format:"ip"`
477+
}{}),
478+
input: map[string]any{"value": "1234"},
479+
errs: []string{"expected string to be either RFC 2673 ipv4 or RFC 2373 ipv6"},
480+
},
459481
{
460482
name: "uri success",
461483
typ: reflect.TypeFor[struct {

validation/messages.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ var (
1111
MsgExpectedRFC5890Hostname = "expected string to be RFC 5890 hostname"
1212
MsgExpectedRFC2673IPv4 = "expected string to be RFC 2673 ipv4"
1313
MsgExpectedRFC2373IPv6 = "expected string to be RFC 2373 ipv6"
14+
MsgExpectedRFCIPAddr = "expected string to be either RFC 2673 ipv4 or RFC 2373 ipv6"
1415
MsgExpectedRFC3986URI = "expected string to be RFC 3986 uri: %v"
1516
MsgExpectedRFC4122UUID = "expected string to be RFC 4122 uuid: %v"
1617
MsgExpectedRFC6570URITemplate = "expected string to be RFC 6570 uri-template"

0 commit comments

Comments
 (0)