Repository: swaggest/rest (go)

Issue:
Unable to set default values for custom typed strings
**Describe the bug**
I'm trying to build an enum type that can be used for input. As a part of this, I'd like to be able to set a default value for the enum field as well. I'd like to be able to define this in code (for reuse) rather than as a struct tag (i.e make our actual inputs/outputs match the documentation).

However, when I try to use a custom type with the default struct tags, it seems as though the parser expects a json string/object rather than a string value. You can use a json string to get past this, but then default does something weird. Concrete example below.

**To Reproduce**
```
package main_test

import (
	"bytes"
	"context"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/swaggest/rest/web"
	"github.com/swaggest/usecase"
)

type Discover string

const (
	DiscoverAll  Discover = "all"
	DiscoverNone Discover = "none"
)

func (d *Discover) Enum() []any {
	return []any{DiscoverAll, DiscoverNone}
}

func TestFoo(t *testing.T) {
	type NewThing struct {
		DiscoverMode *Discover `json:"discover,omitempty" default:"all"`
	}

	s := web.DefaultService()

	s.Post("/foo", usecase.NewInteractor(func(ctx context.Context, input NewThing, output *string) error {
		disc := string(*input.DiscoverMode)
		*output = disc

		return nil
	}))

	req, err := http.NewRequest(http.MethodPost, "/foo", bytes.NewReader([]byte(`{}`)))
	require.NoError(t, err)
	req.Header.Set("Content-Type", "application/json")
	rw := httptest.NewRecorder()

	s.ServeHTTP(rw, req)
	assert.Equal(t, `"all" `, rw.Body.String())
}
```

**Observed behavior**

```
=== RUN   TestFoo
--- FAIL: TestFoo (0.00s)
panic: DiscoverMode: parsing default as JSON: invalid character 'a' looking for beginning of value [recovered]
        panic: DiscoverMode: parsing default as JSON: invalid character 'a' looking for beginning of value
```

**Expected behavior**
The API returns `all`.

Side note: you can change this so it reads `default:"\"all\""` -- then the input validation passes, but your default ends up as the quoted json string `"all"`, not `all` (i.e. it is not possible to produce the valid enum option).


--

I'm happy to make a contribution here if you can point me in the right direction. I did step through the code a bit. I tried Implementing a TextUn/Marshaller so that maybe the type would properly get set as a string in `jsonschema-go/reflect.go`, but that didn't seem to make anything better on the version I was using. I believe it's because string is added as "a" type, rather than "the" SimpleType, which causes the `checkInlineValue` to fall into the `default` rather than `String` case (where it does a `json.Unmarshal`)

Pull request:
Infer default values from prepared schema
Fixes https://github.com/swaggest/rest/issues/133.
Fixes #194.

Patch:
diff --git a/_examples/go.mod b/_examples/go.mod
index 7b75e96..db07f3a 100644
--- a/_examples/go.mod
+++ b/_examples/go.mod
@@ -17,8 +17,8 @@ require (
 	github.com/rs/cors v1.10.1
 	github.com/stretchr/testify v1.9.0
 	github.com/swaggest/assertjson v1.9.0
-	github.com/swaggest/jsonschema-go v0.3.66
-	github.com/swaggest/openapi-go v0.2.47
+	github.com/swaggest/jsonschema-go v0.3.69
+	github.com/swaggest/openapi-go v0.2.49
 	github.com/swaggest/rest v0.0.0-00010101000000-000000000000
 	github.com/swaggest/swgui v1.8.0
 	github.com/swaggest/usecase v1.3.1
diff --git a/_examples/go.sum b/_examples/go.sum
index 7c87d29..a6211c5 100644
--- a/_examples/go.sum
+++ b/_examples/go.sum
@@ -123,10 +123,10 @@ github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7
 github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU=
 github.com/swaggest/form/v5 v5.1.1 h1:ct6/rOQBGrqWUQ0FUv3vW5sHvTUb31AwTUWj947N6cY=
 github.com/swaggest/form/v5 v5.1.1/go.mod h1:X1hraaoONee20PMnGNLQpO32f9zbQ0Czfm7iZThuEKg=
-github.com/swaggest/jsonschema-go v0.3.66 h1:4c5d7NRRqPLTswsbaypKqcMe3Z+CYHE3/lGsPIByp8o=
-github.com/swaggest/jsonschema-go v0.3.66/go.mod h1:7N43/CwdaWgPUDfYV70K7Qm79tRqe/al7gLSt9YeGIE=
-github.com/swaggest/openapi-go v0.2.47 h1:qBh28FHz0M1QSJmGRCcY/Xt9WKRkECKXGUbw/U8IcJ4=
-github.com/swaggest/openapi-go v0.2.47/go.mod h1:MK5O26lG289kFgMOyXK1VXDoTZ89KJ8Vt0v0ic23zZw=
+github.com/swaggest/jsonschema-go v0.3.69 h1:BNEajhoQjnEQzxZqPmjD1Pcs1pxnjx/X9L5KWllLHxo=
+github.com/swaggest/jsonschema-go v0.3.69/go.mod h1:7N43/CwdaWgPUDfYV70K7Qm79tRqe/al7gLSt9YeGIE=
+github.com/swaggest/openapi-go v0.2.49 h1:WxAfdde6DlfQPQayHWTb64CywK9+vpToi7iN17iDPO8=
+github.com/swaggest/openapi-go v0.2.49/go.mod h1:MK5O26lG289kFgMOyXK1VXDoTZ89KJ8Vt0v0ic23zZw=
 github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I=
 github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg=
 github.com/swaggest/swgui v1.8.0 h1:dPu8TsYIOraaObAkyNdoiLI8mu7nOqQ6SU7HOv254rM=
diff --git a/go.mod b/go.mod
index 2fc33be..f05ba3b 100644
--- a/go.mod
+++ b/go.mod
@@ -13,8 +13,8 @@ require (
 	github.com/stretchr/testify v1.8.2
 	github.com/swaggest/assertjson v1.9.0
 	github.com/swaggest/form/v5 v5.1.1
-	github.com/swaggest/jsonschema-go v0.3.66
-	github.com/swaggest/openapi-go v0.2.47
+	github.com/swaggest/jsonschema-go v0.3.69
+	github.com/swaggest/openapi-go v0.2.49
 	github.com/swaggest/refl v1.3.0
 	github.com/swaggest/usecase v1.3.1
 )
diff --git a/go.sum b/go.sum
index ffe9d70..9857b9a 100644
--- a/go.sum
+++ b/go.sum
@@ -78,10 +78,11 @@ github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7
 github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU=
 github.com/swaggest/form/v5 v5.1.1 h1:ct6/rOQBGrqWUQ0FUv3vW5sHvTUb31AwTUWj947N6cY=
 github.com/swaggest/form/v5 v5.1.1/go.mod h1:X1hraaoONee20PMnGNLQpO32f9zbQ0Czfm7iZThuEKg=
-github.com/swaggest/jsonschema-go v0.3.66 h1:4c5d7NRRqPLTswsbaypKqcMe3Z+CYHE3/lGsPIByp8o=
 github.com/swaggest/jsonschema-go v0.3.66/go.mod h1:7N43/CwdaWgPUDfYV70K7Qm79tRqe/al7gLSt9YeGIE=
-github.com/swaggest/openapi-go v0.2.47 h1:qBh28FHz0M1QSJmGRCcY/Xt9WKRkECKXGUbw/U8IcJ4=
-github.com/swaggest/openapi-go v0.2.47/go.mod h1:MK5O26lG289kFgMOyXK1VXDoTZ89KJ8Vt0v0ic23zZw=
+github.com/swaggest/jsonschema-go v0.3.69 h1:BNEajhoQjnEQzxZqPmjD1Pcs1pxnjx/X9L5KWllLHxo=
+github.com/swaggest/jsonschema-go v0.3.69/go.mod h1:7N43/CwdaWgPUDfYV70K7Qm79tRqe/al7gLSt9YeGIE=
+github.com/swaggest/openapi-go v0.2.49 h1:WxAfdde6DlfQPQayHWTb64CywK9+vpToi7iN17iDPO8=
+github.com/swaggest/openapi-go v0.2.49/go.mod h1:MK5O26lG289kFgMOyXK1VXDoTZ89KJ8Vt0v0ic23zZw=
 github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I=
 github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg=
 github.com/swaggest/usecase v1.3.1 h1:JdKV30MTSsDxAXxkldLNcEn8O2uf565khyo6gr5sS+w=
diff --git a/request/factory.go b/request/factory.go
index 940437d..8978bac 100644
--- a/request/factory.go
+++ b/request/factory.go
@@ -11,6 +11,7 @@ import (
 	"strings"
 
 	"github.com/swaggest/form/v5"
+	"github.com/swaggest/jsonschema-go"
 	"github.com/swaggest/openapi-go"
 	"github.com/swaggest/refl"
 	"github.com/swaggest/rest"
@@ -38,6 +39,9 @@ type DecoderFactory struct {
 	// If not set encoding/json.Decoder is used.
 	JSONReader func(rd io.Reader, v interface{}) error
 
+	// JSONSchemaReflector is optional, it is called to infer "default" values.
+	JSONSchemaReflector *jsonschema.Reflector
+
 	formDecoders      map[rest.ParamIn]*form.Decoder
 	decoderFunctions  map[rest.ParamIn]decoderFunc
 	defaultValDecoder *form.Decoder
@@ -257,7 +261,7 @@ func (df *DecoderFactory) jsonParams(formDecoder *form.Decoder, in rest.ParamIn,
 func (df *DecoderFactory) makeDefaultDecoder(input interface{}, m *decoder) {
 	defaults := url.Values{}
 
-	refl.WalkFieldsRecursively(reflect.ValueOf(input), func(_ reflect.Value, sf reflect.StructField, path []reflect.StructField) {
+	refl.WalkFieldsRecursively(reflect.ValueOf(input), func(v reflect.Value, sf reflect.StructField, path []reflect.StructField) {
 		var key string
 
 		for _, p := range path {
@@ -278,8 +282,24 @@ func (df *DecoderFactory) makeDefaultDecoder(input interface{}, m *decoder) {
 			key += "[" + sf.Name + "]"
 		}
 
-		if d, ok := sf.Tag.Lookup(defaultTag); ok {
+		if d, ok := sf.Tag.Lookup(defaultTag); ok { //nolint:nestif
 			defaults[key] = []string{d}
+		} else if df.JSONSchemaReflector != nil {
+			vi := v.Interface()
+
+			s, err := df.JSONSchemaReflector.Reflect(vi)
+			if err != nil {
+				panic(err.Error())
+			}
+
+			if s.Default != nil {
+				d, err := json.Marshal(s.Default)
+				if err != nil {
+					panic(err.Error())
+				}
+
+				defaults[key] = []string{strings.Trim(string(d), `"`)}
+			}
 		}
 	})
 
diff --git a/web/service.go b/web/service.go
index d907caa..eb0a973 100644
--- a/web/service.go
+++ b/web/service.go
@@ -44,6 +44,7 @@ func NewService(refl oapi.Reflector, options ...func(s *Service)) *Service {
 	if s.DecoderFactory == nil {
 		decoderFactory := request.NewDecoderFactory()
 		decoderFactory.ApplyDefaults = true
+		decoderFactory.JSONSchemaReflector = s.OpenAPICollector.Refl().JSONSchemaReflector()
 		decoderFactory.SetDecoderFunc(rest.ParamInPath, chirouter.PathToURLValues)
 
 		s.DecoderFactory = decoderFactory


Test patch:
diff --git a/request/factory_test.go b/request/factory_test.go
index 765fdae..ebad192 100644
--- a/request/factory_test.go
+++ b/request/factory_test.go
@@ -1,6 +1,7 @@
 package request_test
 
 import (
+	"bytes"
 	"encoding/json"
 	"fmt"
 	"net/http"
@@ -10,6 +11,7 @@ import (
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	"github.com/swaggest/jsonschema-go"
 	"github.com/swaggest/rest"
 	"github.com/swaggest/rest/request"
 )
@@ -269,3 +271,68 @@ func TestDecoderFactory_MakeDecoder_header_case_sensitivity(t *testing.T) {
 	assert.Equal(t, "hello!", v.C)
 	assert.Equal(t, "hello!", v.D)
 }
+
+type defaultFromSchema string
+
+func (d *defaultFromSchema) PrepareJSONSchema(schema *jsonschema.Schema) error {
+	schema.WithDefault(enum1)
+	schema.WithTitle("Value with default from schema")
+
+	return nil
+}
+
+type defaultFromSchemaVal string
+
+func (d defaultFromSchemaVal) PrepareJSONSchema(schema *jsonschema.Schema) error {
+	schema.WithDefault(enum1)
+	schema.WithTitle("Value with default from schema")
+
+	return nil
+}
+
+const (
+	enum1 = "all"
+	enum2 = "none"
+)
+
+func (d *defaultFromSchema) Enum() []interface{} {
+	return []interface{}{enum1, enum2}
+}
+
+func (d defaultFromSchemaVal) Enum() []interface{} {
+	return []interface{}{enum1, enum2}
+}
+
+func TestNewDecoderFactory_default(t *testing.T) {
+	type NewThing struct {
+		DefaultedQuery    *defaultFromSchema    `query:"dq"`
+		DefaultedPtr      *defaultFromSchema    `json:"dp,omitempty"`
+		Defaulted         defaultFromSchema     `json:"d"`
+		DefaultedTag      defaultFromSchema     `query:"dt" default:"none"`
+		DefaultedQueryVal *defaultFromSchemaVal `query:"dqv"`
+		DefaultedPtrVal   *defaultFromSchemaVal `json:"dpv,omitempty"`
+		DefaultedVal      defaultFromSchemaVal  `json:"dv"`
+		DefaultedTagVal   defaultFromSchemaVal  `query:"dtv" default:"none"`
+	}
+
+	df := request.NewDecoderFactory()
+	df.ApplyDefaults = true
+	df.JSONSchemaReflector = &jsonschema.Reflector{}
+
+	var input NewThing
+	dec := df.MakeDecoder(http.MethodPost, input, nil)
+
+	req, err := http.NewRequest(http.MethodPost, "/foo", bytes.NewReader([]byte(`{}`)))
+	require.NoError(t, err)
+
+	require.NoError(t, dec.Decode(req, &input, nil))
+	assert.Equal(t, enum1, string(*input.DefaultedPtr))
+	assert.Equal(t, enum1, string(input.Defaulted))
+	assert.Equal(t, enum1, string(*input.DefaultedQuery))
+	assert.Equal(t, enum2, string(input.DefaultedTag))
+
+	assert.Equal(t, enum1, string(*input.DefaultedPtrVal))
+	assert.Equal(t, enum1, string(input.DefaultedVal))
+	assert.Equal(t, enum1, string(*input.DefaultedQueryVal))
+	assert.Equal(t, enum2, string(input.DefaultedTagVal))
+}