Repository: ogen-go/ogen
Pull request #718 (2022-12-10 22:02:34)

feat(gen): additional go struct field tags
Implements https://github.com/ogen-go/ogen/issues/689
See usage in README.md

codecov[bot]: # [Codecov](https://codecov.io/gh/ogen-go/ogen/pull/718?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) Report
Base: **74.27**% // Head: **74.26**% // Decreases project coverage by **`-0.01%`** :warning:
> Coverage data is based on head [(`91e2d8a`)](https://codecov.io/gh/ogen-go/ogen/pull/718?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) compared to base [(`4ac1c7c`)](https://codecov.io/gh/ogen-go/ogen/commit/4ac1c7c79187f553fc89610010e0068231ca4ebd?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go).
> Patch coverage: 80.00% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #718      +/-   ##
==========================================
- Coverage   74.27%   74.26%   -0.02%     
==========================================
  Files         175      176       +1     
  Lines       12693    12709      +16     
==========================================
+ Hits         9428     9438      +10     
- Misses       2783     2786       +3     
- Partials      482      485       +3     
```


| [Impacted Files](https://codecov.io/gh/ogen-go/ogen/pull/718?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) | Coverage Δ | |
|---|---|---|
| [gen/ir/field.go](https://codecov.io/gh/ogen-go/ogen/pull/718/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-Z2VuL2lyL2ZpZWxkLmdv) | `95.31% <ø> (ø)` | |
| [jsonschema/schema.go](https://codecov.io/gh/ogen-go/ogen/pull/718/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-anNvbnNjaGVtYS9zY2hlbWEuZ28=) | `100.00% <ø> (ø)` | |
| [internal/math/math.go](https://codecov.io/gh/ogen-go/ogen/pull/718/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-aW50ZXJuYWwvbWF0aC9tYXRoLmdv) | `40.00% <40.00%> (ø)` | |
| [jsonschema/parser.go](https://codecov.io/gh/ogen-go/ogen/pull/718/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-anNvbnNjaGVtYS9wYXJzZXIuZ28=) | `84.35% <66.66%> (-0.59%)` | :arrow_down: |
| [gen/gen\_contents.go](https://codecov.io/gh/ogen-go/ogen/pull/718/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-Z2VuL2dlbl9jb250ZW50cy5nbw==) | `74.35% <100.00%> (-0.65%)` | :arrow_down: |
| [gen/schema\_gen.go](https://codecov.io/gh/ogen-go/ogen/pull/718/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-Z2VuL3NjaGVtYV9nZW4uZ28=) | `82.73% <100.00%> (+0.42%)` | :arrow_up: |
| [gen/templates.go](https://codecov.io/gh/ogen-go/ogen/pull/718/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-Z2VuL3RlbXBsYXRlcy5nbw==) | `82.73% <100.00%> (ø)` | |
| [gen/generator.go](https://codecov.io/gh/ogen-go/ogen/pull/718/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-Z2VuL2dlbmVyYXRvci5nbw==) | `59.87% <0.00%> (+1.23%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/ogen-go/ogen/pull/718?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go).


Patch:
diff --git a/Makefile b/Makefile
index ced1303e..894cc04f 100644
--- a/Makefile
+++ b/Makefile
@@ -30,6 +30,11 @@ tidy:
 tidy_examples:
 	cd examples && go mod tidy
 
+lint:
+	@go version
+	@golangci-lint version
+	golangci-lint run -v -c .golangci.yml ./...
+
 clean: tidy generate tidy_examples examples
 
 commit_gen:
diff --git a/README.md b/README.md
index 6bc8a68e..5e9260be 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,7 @@ go get -d github.com/ogen-go/ogen
   * Primitive types (`string`, `number`) are detected by type
   * Discriminator field is used if defined in schema
   * Type is inferred by unique fields if possible
+* Extra Go struct field tags in the generated types
 * OpenTelemetry tracing and metrics
 
 Example generated structure from schema:
@@ -137,6 +138,92 @@ func NewStringID(v string) ID
 func NewIntID(v int) ID
 ```
 
+## Extension properties
+
+OpenAPI enables [Specification Extensions](https://spec.openapis.org/oas/v3.1.0#specification-extensions),
+which are implemented as patterned fields that are always prefixed by `x-`.
+
+### Server name
+
+Optionally, server name can be specified by `x-ogen-server-name`, for example:
+
+```json
+{
+  "openapi": "3.0.3",
+  "servers": [
+    {
+      "x-ogen-server-name": "production",
+      "url": "https://{region}.example.com/{val}/v1",
+    },
+    {
+      "x-ogen-server-name": "prefix",
+      "url": "/{val}/v1",
+    },
+    {
+      "x-ogen-server-name": "const",
+      "url": "https://cdn.example.com/v1"
+    }
+  ],
+(...)
+```
+
+### Custom type name
+
+Optionally, type name can be specified by `x-ogen-name`, for example:
+
+```json
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "type": "object",
+  "x-ogen-name": "Name",
+  "properties": {
+    "foobar": {
+      "$ref": "#/$defs/FooBar"
+    }
+  },
+  "$defs": {
+    "FooBar": {
+      "x-ogen-name": "FooBar",
+      "type": "object",
+      "properties": {
+        "foo": {
+          "type": "string"
+        }
+      }
+    }
+  }
+}
+```
+
+### Extra struct field tags
+
+Optionally, additional Go struct field tags can be specified by `x-oapi-codegen-extra-tags`, for example:
+
+```yaml
+components:
+  schemas:
+    Pet:
+      type: object
+      required:
+        - id
+      properties:
+        id:
+          type: integer
+          format: int64
+          x-oapi-codegen-extra-tags:
+            gorm: primaryKey
+            valid: customIdValidator
+```
+
+The generated source code looks like:
+
+```go
+// Ref: #/components/schemas/Pet
+type Pet struct {
+    ID   int64     `gorm:"primaryKey" valid:"customNameValidator" json:"id"`
+}
+```
+
 ## JSON
 
 Code generation provides very efficient and flexible encoding and decoding of json:
diff --git a/gen/_template/schema/struct.tmpl b/gen/_template/schema/struct.tmpl
index 806c2ec4..65c2146f 100644
--- a/gen/_template/schema/struct.tmpl
+++ b/gen/_template/schema/struct.tmpl
@@ -6,7 +6,7 @@
 	type {{ $.Name }} struct {
 	{{- range $f := $.Fields }}
 		{{- template "godoc" $f.GoDoc }}
-		{{ $f.Name }} {{ $f.Type.Go }} {{ if $f.Tag.JSON }}{{ printf "json:%q" $f.Tag.JSON | backquote }}{{ end }}
+		{{ $f.Name }} {{ $f.Type.Go }} {{ if $f.Tag.GetTags }}{{ backquote $f.Tag.GetTags }}{{ end }}
 	{{- end }}
 	}
 {{- end }}
diff --git a/gen/ir/field.go b/gen/ir/field.go
index 4f372889..cc87bbe1 100644
--- a/gen/ir/field.go
+++ b/gen/ir/field.go
@@ -3,17 +3,20 @@ package ir
 import (
 	"fmt"
 	"strconv"
+	"strings"
 
 	"golang.org/x/exp/slices"
 
+	"github.com/ogen-go/ogen/internal/xmaps"
 	"github.com/ogen-go/ogen/jsonschema"
 	"github.com/ogen-go/ogen/openapi"
 )
 
 // Tag of Field.
 type Tag struct {
-	JSON string             // json tag, empty for none
-	Form *openapi.Parameter // query form parameter
+	JSON      string             // json tag, empty for none
+	Form      *openapi.Parameter // query form parameter
+	ExtraTags map[string]string  // a map of extra struct field tags
 }
 
 // EscapedJSON returns quoted and escaped JSON tag.
@@ -21,6 +24,21 @@ func (t Tag) EscapedJSON() string {
 	return strconv.Quote(t.JSON)
 }
 
+// GetTags returns a formatted list of struct tags, which must be quoted by '`'
+func (t Tag) GetTags() string {
+	tags := make([]string, 0, 1+len(t.ExtraTags))
+	if t.JSON != "" {
+		tags = append(tags, fmt.Sprintf(`%s:%q`, "json", t.JSON))
+	}
+	if t.ExtraTags != nil {
+		for _, k := range xmaps.SortedKeys(t.ExtraTags) {
+			tags = append(tags, fmt.Sprintf(`%s:%q`, k, t.ExtraTags[k]))
+		}
+	}
+
+	return strings.Join(tags, " ")
+}
+
 // InlineField defines how to inline field.
 type InlineField int
 
diff --git a/gen/schema_gen.go b/gen/schema_gen.go
index 7e03fd21..a1ad3a37 100644
--- a/gen/schema_gen.go
+++ b/gen/schema_gen.go
@@ -198,7 +198,8 @@ func (g *schemaGen) generate2(name string, schema *jsonschema.Schema) (ret *ir.T
 				Name: fieldName,
 				Type: t,
 				Tag: ir.Tag{
-					JSON: prop.Name,
+					JSON:      prop.Name,
+					ExtraTags: prop.Schema.ExtraTags,
 				},
 				Spec: &prop,
 			})
diff --git a/go.mod b/go.mod
index 5e8d49ee..21dec09a 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/ogen-go/ogen
 go 1.18
 
 require (
+	github.com/davecgh/go-spew v1.1.1
 	github.com/dlclark/regexp2 v1.8.0
 	github.com/fatih/color v1.14.1
 	github.com/ghodss/yaml v1.0.0
@@ -27,7 +28,6 @@ require (
 require (
 	github.com/andybalholm/brotli v1.0.4 // indirect
 	github.com/benbjohnson/clock v1.3.0 // indirect
-	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/go-logr/logr v1.2.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/klauspost/compress v1.15.9 // indirect
diff --git a/internal/integration/sample_api/oas_faker_gen.go b/internal/integration/sample_api/oas_faker_gen.go
index ab9ca99d..3a2dbb9f 100644
--- a/internal/integration/sample_api/oas_faker_gen.go
+++ b/internal/integration/sample_api/oas_faker_gen.go
@@ -1207,6 +1207,11 @@ func (s *Pet) SetFake() {
 			s.NullValue.SetFake()
 		}
 	}
+	{
+		{
+			s.ExtraTags.SetFake()
+		}
+	}
 }
 
 // SetFake set fake values.
diff --git a/internal/integration/sample_api/oas_json_gen.go b/internal/integration/sample_api/oas_json_gen.go
index eb423cfd..b007ff51 100644
--- a/internal/integration/sample_api/oas_json_gen.go
+++ b/internal/integration/sample_api/oas_json_gen.go
@@ -5954,9 +5954,15 @@ func (s *Pet) encodeFields(e *jx.Encoder) {
 			s.NullValue.Encode(e)
 		}
 	}
+	{
+		if s.ExtraTags.Set {
+			e.FieldStart("extraTags")
+			s.ExtraTags.Encode(e)
+		}
+	}
 }
 
-var jsonFieldsNameOfPet = [30]string{
+var jsonFieldsNameOfPet = [31]string{
 	0:  "primary",
 	1:  "id",
 	2:  "unique_id",
@@ -5987,6 +5993,7 @@ var jsonFieldsNameOfPet = [30]string{
 	27: "testTime",
 	28: "testDateTime",
 	29: "nullValue",
+	30: "extraTags",
 }
 
 // Decode decodes Pet from json.
@@ -6342,6 +6349,16 @@ func (s *Pet) Decode(d *jx.Decoder) error {
 			}(); err != nil {
 				return errors.Wrap(err, "decode field \"nullValue\"")
 			}
+		case "extraTags":
+			if err := func() error {
+				s.ExtraTags.Reset()
+				if err := s.ExtraTags.Decode(d); err != nil {
+					return err
+				}
+				return nil
+			}(); err != nil {
+				return errors.Wrap(err, "decode field \"extraTags\"")
+			}
 		default:
 			return d.Skip()
 		}
diff --git a/internal/integration/sample_api/oas_schemas_gen.go b/internal/integration/sample_api/oas_schemas_gen.go
index 072b531b..eae35c1d 100644
--- a/internal/integration/sample_api/oas_schemas_gen.go
+++ b/internal/integration/sample_api/oas_schemas_gen.go
@@ -3922,6 +3922,7 @@ type Pet struct {
 	TestTime          OptTime              `json:"testTime"`
 	TestDateTime      OptDateTime          `json:"testDateTime"`
 	NullValue         OptNullValue         `json:"nullValue"`
+	ExtraTags         OptString            `json:"extraTags" gorm:"index" valid:"customValidator"`
 }
 
 // GetPrimary returns the value of Primary.
@@ -4074,6 +4075,11 @@ func (s *Pet) GetNullValue() OptNullValue {
 	return s.NullValue
 }
 
+// GetExtraTags returns the value of ExtraTags.
+func (s *Pet) GetExtraTags() OptString {
+	return s.ExtraTags
+}
+
 // SetPrimary sets the value of Primary.
 func (s *Pet) SetPrimary(val *Pet) {
 	s.Primary = val
@@ -4224,6 +4230,11 @@ func (s *Pet) SetNullValue(val OptNullValue) {
 	s.NullValue = val
 }
 
+// SetExtraTags sets the value of ExtraTags.
+func (s *Pet) SetExtraTags(val OptString) {
+	s.ExtraTags = val
+}
+
 func (*Pet) foobarGetRes()  {}
 func (*Pet) foobarPostRes() {}
 func (*Pet) petGetRes()     {}
diff --git a/internal/integration/sample_api_nc/oas_json_gen.go b/internal/integration/sample_api_nc/oas_json_gen.go
index eb423cfd..b007ff51 100644
--- a/internal/integration/sample_api_nc/oas_json_gen.go
+++ b/internal/integration/sample_api_nc/oas_json_gen.go
@@ -5954,9 +5954,15 @@ func (s *Pet) encodeFields(e *jx.Encoder) {
 			s.NullValue.Encode(e)
 		}
 	}
+	{
+		if s.ExtraTags.Set {
+			e.FieldStart("extraTags")
+			s.ExtraTags.Encode(e)
+		}
+	}
 }
 
-var jsonFieldsNameOfPet = [30]string{
+var jsonFieldsNameOfPet = [31]string{
 	0:  "primary",
 	1:  "id",
 	2:  "unique_id",
@@ -5987,6 +5993,7 @@ var jsonFieldsNameOfPet = [30]string{
 	27: "testTime",
 	28: "testDateTime",
 	29: "nullValue",
+	30: "extraTags",
 }
 
 // Decode decodes Pet from json.
@@ -6342,6 +6349,16 @@ func (s *Pet) Decode(d *jx.Decoder) error {
 			}(); err != nil {
 				return errors.Wrap(err, "decode field \"nullValue\"")
 			}
+		case "extraTags":
+			if err := func() error {
+				s.ExtraTags.Reset()
+				if err := s.ExtraTags.Decode(d); err != nil {
+					return err
+				}
+				return nil
+			}(); err != nil {
+				return errors.Wrap(err, "decode field \"extraTags\"")
+			}
 		default:
 			return d.Skip()
 		}
diff --git a/internal/integration/sample_api_nc/oas_schemas_gen.go b/internal/integration/sample_api_nc/oas_schemas_gen.go
index 072b531b..eae35c1d 100644
--- a/internal/integration/sample_api_nc/oas_schemas_gen.go
+++ b/internal/integration/sample_api_nc/oas_schemas_gen.go
@@ -3922,6 +3922,7 @@ type Pet struct {
 	TestTime          OptTime              `json:"testTime"`
 	TestDateTime      OptDateTime          `json:"testDateTime"`
 	NullValue         OptNullValue         `json:"nullValue"`
+	ExtraTags         OptString            `json:"extraTags" gorm:"index" valid:"customValidator"`
 }
 
 // GetPrimary returns the value of Primary.
@@ -4074,6 +4075,11 @@ func (s *Pet) GetNullValue() OptNullValue {
 	return s.NullValue
 }
 
+// GetExtraTags returns the value of ExtraTags.
+func (s *Pet) GetExtraTags() OptString {
+	return s.ExtraTags
+}
+
 // SetPrimary sets the value of Primary.
 func (s *Pet) SetPrimary(val *Pet) {
 	s.Primary = val
@@ -4224,6 +4230,11 @@ func (s *Pet) SetNullValue(val OptNullValue) {
 	s.NullValue = val
 }
 
+// SetExtraTags sets the value of ExtraTags.
+func (s *Pet) SetExtraTags(val OptString) {
+	s.ExtraTags = val
+}
+
 func (*Pet) foobarGetRes()  {}
 func (*Pet) foobarPostRes() {}
 func (*Pet) petGetRes()     {}
diff --git a/internal/integration/sample_api_ns/oas_json_gen.go b/internal/integration/sample_api_ns/oas_json_gen.go
index eb423cfd..b007ff51 100644
--- a/internal/integration/sample_api_ns/oas_json_gen.go
+++ b/internal/integration/sample_api_ns/oas_json_gen.go
@@ -5954,9 +5954,15 @@ func (s *Pet) encodeFields(e *jx.Encoder) {
 			s.NullValue.Encode(e)
 		}
 	}
+	{
+		if s.ExtraTags.Set {
+			e.FieldStart("extraTags")
+			s.ExtraTags.Encode(e)
+		}
+	}
 }
 
-var jsonFieldsNameOfPet = [30]string{
+var jsonFieldsNameOfPet = [31]string{
 	0:  "primary",
 	1:  "id",
 	2:  "unique_id",
@@ -5987,6 +5993,7 @@ var jsonFieldsNameOfPet = [30]string{
 	27: "testTime",
 	28: "testDateTime",
 	29: "nullValue",
+	30: "extraTags",
 }
 
 // Decode decodes Pet from json.
@@ -6342,6 +6349,16 @@ func (s *Pet) Decode(d *jx.Decoder) error {
 			}(); err != nil {
 				return errors.Wrap(err, "decode field \"nullValue\"")
 			}
+		case "extraTags":
+			if err := func() error {
+				s.ExtraTags.Reset()
+				if err := s.ExtraTags.Decode(d); err != nil {
+					return err
+				}
+				return nil
+			}(); err != nil {
+				return errors.Wrap(err, "decode field \"extraTags\"")
+			}
 		default:
 			return d.Skip()
 		}
diff --git a/internal/integration/sample_api_ns/oas_schemas_gen.go b/internal/integration/sample_api_ns/oas_schemas_gen.go
index 072b531b..eae35c1d 100644
--- a/internal/integration/sample_api_ns/oas_schemas_gen.go
+++ b/internal/integration/sample_api_ns/oas_schemas_gen.go
@@ -3922,6 +3922,7 @@ type Pet struct {
 	TestTime          OptTime              `json:"testTime"`
 	TestDateTime      OptDateTime          `json:"testDateTime"`
 	NullValue         OptNullValue         `json:"nullValue"`
+	ExtraTags         OptString            `json:"extraTags" gorm:"index" valid:"customValidator"`
 }
 
 // GetPrimary returns the value of Primary.
@@ -4074,6 +4075,11 @@ func (s *Pet) GetNullValue() OptNullValue {
 	return s.NullValue
 }
 
+// GetExtraTags returns the value of ExtraTags.
+func (s *Pet) GetExtraTags() OptString {
+	return s.ExtraTags
+}
+
 // SetPrimary sets the value of Primary.
 func (s *Pet) SetPrimary(val *Pet) {
 	s.Primary = val
@@ -4224,6 +4230,11 @@ func (s *Pet) SetNullValue(val OptNullValue) {
 	s.NullValue = val
 }
 
+// SetExtraTags sets the value of ExtraTags.
+func (s *Pet) SetExtraTags(val OptString) {
+	s.ExtraTags = val
+}
+
 func (*Pet) foobarGetRes()  {}
 func (*Pet) foobarPostRes() {}
 func (*Pet) petGetRes()     {}
diff --git a/internal/integration/sample_api_nsnc/oas_json_gen.go b/internal/integration/sample_api_nsnc/oas_json_gen.go
index eb423cfd..b007ff51 100644
--- a/internal/integration/sample_api_nsnc/oas_json_gen.go
+++ b/internal/integration/sample_api_nsnc/oas_json_gen.go
@@ -5954,9 +5954,15 @@ func (s *Pet) encodeFields(e *jx.Encoder) {
 			s.NullValue.Encode(e)
 		}
 	}
+	{
+		if s.ExtraTags.Set {
+			e.FieldStart("extraTags")
+			s.ExtraTags.Encode(e)
+		}
+	}
 }
 
-var jsonFieldsNameOfPet = [30]string{
+var jsonFieldsNameOfPet = [31]string{
 	0:  "primary",
 	1:  "id",
 	2:  "unique_id",
@@ -5987,6 +5993,7 @@ var jsonFieldsNameOfPet = [30]string{
 	27: "testTime",
 	28: "testDateTime",
 	29: "nullValue",
+	30: "extraTags",
 }
 
 // Decode decodes Pet from json.
@@ -6342,6 +6349,16 @@ func (s *Pet) Decode(d *jx.Decoder) error {
 			}(); err != nil {
 				return errors.Wrap(err, "decode field \"nullValue\"")
 			}
+		case "extraTags":
+			if err := func() error {
+				s.ExtraTags.Reset()
+				if err := s.ExtraTags.Decode(d); err != nil {
+					return err
+				}
+				return nil
+			}(); err != nil {
+				return errors.Wrap(err, "decode field \"extraTags\"")
+			}
 		default:
 			return d.Skip()
 		}
diff --git a/internal/integration/sample_api_nsnc/oas_schemas_gen.go b/internal/integration/sample_api_nsnc/oas_schemas_gen.go
index 072b531b..eae35c1d 100644
--- a/internal/integration/sample_api_nsnc/oas_schemas_gen.go
+++ b/internal/integration/sample_api_nsnc/oas_schemas_gen.go
@@ -3922,6 +3922,7 @@ type Pet struct {
 	TestTime          OptTime              `json:"testTime"`
 	TestDateTime      OptDateTime          `json:"testDateTime"`
 	NullValue         OptNullValue         `json:"nullValue"`
+	ExtraTags         OptString            `json:"extraTags" gorm:"index" valid:"customValidator"`
 }
 
 // GetPrimary returns the value of Primary.
@@ -4074,6 +4075,11 @@ func (s *Pet) GetNullValue() OptNullValue {
 	return s.NullValue
 }
 
+// GetExtraTags returns the value of ExtraTags.
+func (s *Pet) GetExtraTags() OptString {
+	return s.ExtraTags
+}
+
 // SetPrimary sets the value of Primary.
 func (s *Pet) SetPrimary(val *Pet) {
 	s.Primary = val
@@ -4224,6 +4230,11 @@ func (s *Pet) SetNullValue(val OptNullValue) {
 	s.NullValue = val
 }
 
+// SetExtraTags sets the value of ExtraTags.
+func (s *Pet) SetExtraTags(val OptString) {
+	s.ExtraTags = val
+}
+
 func (*Pet) foobarGetRes()  {}
 func (*Pet) foobarPostRes() {}
 func (*Pet) petGetRes()     {}
diff --git a/jsonschema/parser.go b/jsonschema/parser.go
index 609ffa81..05faf083 100644
--- a/jsonschema/parser.go
+++ b/jsonschema/parser.go
@@ -15,6 +15,11 @@ import (
 	"github.com/ogen-go/ogen/ogenregex"
 )
 
+const (
+	xOgenName      = "x-ogen-name"
+	xOapiExtraTags = "x-oapi-codegen-extra-tags"
+)
+
 // Parser parses JSON schemas.
 type Parser struct {
 	external ExternalResolver
@@ -125,9 +130,14 @@ func (p *Parser) parse1(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hook fun
 			return nil, p.wrapField("default", p.file(ctx), schema.Common.Locator, err)
 		}
 	}
-	if a, ok := schema.Common.Extensions["x-ogen-name"]; ok {
+	if a, ok := schema.Common.Extensions[xOgenName]; ok {
 		if err := a.Decode(&s.XOgenName); err != nil {
-			return nil, errors.Wrap(err, "parse x-ogen-name")
+			return nil, errors.Wrap(err, "parse "+xOgenName)
+		}
+	}
+	if a, ok := schema.Common.Extensions[xOapiExtraTags]; ok {
+		if err := a.Decode(&s.ExtraTags); err != nil {
+			return nil, errors.Wrap(err, "parse "+xOapiExtraTags)
 		}
 	}
 
diff --git a/jsonschema/schema.go b/jsonschema/schema.go
index b088fb08..7010c444 100644
--- a/jsonschema/schema.go
+++ b/jsonschema/schema.go
@@ -87,6 +87,9 @@ type Schema struct {
 	Default    any
 	DefaultSet bool
 
+	// ExtraTags is a map of extra struct field tags
+	ExtraTags map[string]string
+
 	location.Pointer `json:"-" yaml:"-"`
 }
 


Test patch:
diff --git a/_testdata/positive/sample.json b/_testdata/positive/sample.json
index 476084dc..26b3ece6 100644
--- a/_testdata/positive/sample.json
+++ b/_testdata/positive/sample.json
@@ -1139,6 +1139,13 @@
           },
           "nullValue": {
             "$ref": "#/components/schemas/NullValue"
+          },
+          "extraTags": {
+            "type": "string",
+            "x-oapi-codegen-extra-tags": {
+              "gorm": "index",
+              "valid": "customValidator"
+            }
           }
         },
         "required": [
diff --git a/gen/ir/field_test.go b/gen/ir/field_test.go
new file mode 100644
index 00000000..c6e72fa1
--- /dev/null
+++ b/gen/ir/field_test.go
@@ -0,0 +1,64 @@
+package ir
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestTagGetTags(t *testing.T) {
+	tests := []struct {
+		name     string
+		tag      Tag
+		wantTags string
+	}{
+		{
+			"Empty",
+			Tag{},
+			``,
+		},
+		{
+			"Only JSON",
+			Tag{
+				JSON: "field",
+			},
+			`json:"field"`,
+		},
+		{
+			"Only ExtraTags",
+			Tag{
+				ExtraTags: map[string]string{
+					"gorm": "primaryKey",
+				},
+			},
+			`gorm:"primaryKey"`,
+		},
+		{
+			"JSON+ExtraTags",
+			Tag{
+				JSON: "field",
+				ExtraTags: map[string]string{
+					"gorm": "primaryKey",
+				},
+			},
+			`json:"field" gorm:"primaryKey"`,
+		},
+		{
+			"JSON+ExtraTags2",
+			Tag{
+				JSON: "field",
+				ExtraTags: map[string]string{
+					"gorm":  "primaryKey",
+					"valid": "customIdValidator",
+				},
+			},
+			`json:"field" gorm:"primaryKey" valid:"customIdValidator"`,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			tags := tt.tag.GetTags()
+			require.Equal(t, tt.wantTags, tags)
+		})
+	}
+}
diff --git a/gen/schema_gen_test.go b/gen/schema_gen_test.go
index c5fa404b..f885f5f8 100644
--- a/gen/schema_gen_test.go
+++ b/gen/schema_gen_test.go
@@ -1,13 +1,17 @@
 package gen
 
 import (
+	"fmt"
 	"testing"
 
+	yaml "github.com/go-faster/yamlx"
 	"github.com/stretchr/testify/require"
 	"go.uber.org/zap"
 	"go.uber.org/zap/zaptest/observer"
 
 	"github.com/ogen-go/ogen/gen/ir"
+	"github.com/ogen-go/ogen/internal/jsonpointer"
+	"github.com/ogen-go/ogen/internal/location"
 	"github.com/ogen-go/ogen/jsonschema"
 )
 
@@ -30,3 +34,166 @@ func TestSchemaGenAnyWarn(t *testing.T) {
 	args := entries[0].ContextMap()
 	a.Equal("foo", args["name"])
 }
+
+func TestGenerate(t *testing.T) {
+	var loc location.Locator
+	loc.UnmarshalYAML(&yaml.Node{
+		Line:   1,
+		Column: 1,
+	})
+	pointer := location.Pointer{
+		Source: location.File{
+			Name:   "pet-tags.yml",
+			Source: "pet-tags.yml",
+		},
+		Locator: loc,
+	}
+
+	tests := []struct {
+		name           string
+		schema         *jsonschema.Schema
+		optional       bool
+		expectedIrType func(*jsonschema.Schema) *ir.Type
+		expectedErr    string
+	}{
+		{
+			name: "fieldTag",
+			schema: &jsonschema.Schema{
+				Ref: jsonpointer.RefKey{
+					Loc: "jsonschema://dummy",
+					Ptr: "#/components/schemas/Pet",
+				},
+				Type: "object",
+				Properties: []jsonschema.Property{
+					{
+						Name: "id",
+						Schema: &jsonschema.Schema{
+							Type:   "integer",
+							Format: "int64",
+							ExtraTags: map[string]string{
+								"gorm":  "primaryKey",
+								"valid": "customIdValidator",
+							},
+							Pointer: pointer,
+						},
+						Required: true,
+					},
+
+					{
+						Name: "name",
+						Schema: &jsonschema.Schema{
+							Type: "string",
+							ExtraTags: map[string]string{
+								"valid": "customNameValidator",
+							},
+							Pointer: pointer,
+						},
+						Required: true,
+					},
+
+					{
+						Name: "tag",
+						Schema: &jsonschema.Schema{
+							Type:    "string",
+							Pointer: pointer,
+						},
+						Required: true,
+					},
+				},
+			},
+			optional:    false,
+			expectedErr: "",
+			expectedIrType: func(schema *jsonschema.Schema) *ir.Type {
+				return &ir.Type{
+					Kind: "struct",
+					Name: "Pet",
+					Fields: []*ir.Field{
+						{
+							Name: "ID",
+							Type: &ir.Type{
+								Kind:      "primitive",
+								Primitive: ir.Int64,
+								Schema:    schema.Properties[0].Schema,
+							},
+							Tag: ir.Tag{
+								JSON: "id",
+								ExtraTags: map[string]string{
+									"gorm":  "primaryKey",
+									"valid": "customIdValidator",
+								},
+							},
+							Spec: &jsonschema.Property{
+								Name:     "id",
+								Schema:   schema.Properties[0].Schema,
+								Required: true,
+							},
+						},
+
+						{
+							Name: "Name",
+							Type: &ir.Type{
+								Kind:      "primitive",
+								Primitive: ir.String,
+								Schema:    schema.Properties[1].Schema,
+							},
+							Tag: ir.Tag{
+								JSON: "name",
+								ExtraTags: map[string]string{
+									"valid": "customNameValidator",
+								},
+							},
+							Spec: &jsonschema.Property{
+								Name:     "name",
+								Schema:   schema.Properties[1].Schema,
+								Required: true,
+							},
+						},
+
+						{
+							Name: "Tag",
+							Type: &ir.Type{
+								Kind:      "primitive",
+								Primitive: ir.String,
+								Schema:    schema.Properties[2].Schema,
+							},
+							Tag: ir.Tag{
+								JSON: "tag",
+							},
+							Spec: &jsonschema.Property{
+								Name:     "tag",
+								Schema:   schema.Properties[2].Schema,
+								Required: true,
+							},
+						},
+					},
+					Schema: schema,
+				}
+			},
+		},
+	}
+	for _, tt := range tests {
+		tt := tt
+		t.Run(fmt.Sprintf("Test %s", tt.name), func(t *testing.T) {
+			a := require.New(t)
+
+			core, _ := observer.New(zap.InfoLevel)
+			s := newSchemaGen(func(ref jsonschema.Ref) (*ir.Type, bool) {
+				return nil, false
+			})
+			s.log = zap.New(core)
+
+			irType, err := s.generate(tt.name, tt.schema, tt.optional)
+
+			var errText string
+			if err != nil {
+				errText = err.Error()
+			}
+			a.Equal(tt.expectedErr, errText, "err")
+
+			expectedIrType := tt.expectedIrType(tt.schema)
+			expectedIrTypeY, _ := yaml.Marshal(expectedIrType)
+			irTypeY, _ := yaml.Marshal(irType)
+			a.Equal(expectedIrType, irType, fmt.Sprintf("\nEXPECTED:\n\n%s\nACTUAL:\n\n%s", expectedIrTypeY, irTypeY))
+		})
+	}
+}
diff --git a/openapi/parser/_testdata/remotes/api/pet-tags.yml b/openapi/parser/_testdata/remotes/api/pet-tags.yml
new file mode 100644
index 00000000..c44068e0
--- /dev/null
+++ b/openapi/parser/_testdata/remotes/api/pet-tags.yml
@@ -0,0 +1,24 @@
+openapi: "3.0.3"
+info:
+  version: 1.0.0
+  title: Swagger Petstore
+components:
+  schemas:
+    Pet:
+      type: object
+      required:
+        - id
+        - name
+      properties:
+        id:
+          type: integer
+          format: int64
+          x-oapi-codegen-extra-tags:
+            gorm: primaryKey
+            valid: customIdValidator
+        name:
+          type: string
+          x-oapi-codegen-extra-tags:
+            valid: customNameValidator
+        tag:
+          type: string
diff --git a/openapi/parser/parser_test.go b/openapi/parser/parser_test.go
index a9d8488c..579ce0db 100644
--- a/openapi/parser/parser_test.go
+++ b/openapi/parser/parser_test.go
@@ -2,15 +2,23 @@ package parser_test
 
 import (
 	"embed"
+	"fmt"
 	"path"
+	"reflect"
 	"strings"
 	"testing"
+	"time"
 
+	"github.com/davecgh/go-spew/spew"
+	yaml "github.com/go-faster/yamlx"
 	"github.com/stretchr/testify/require"
 
 	"github.com/ogen-go/ogen"
+	"github.com/ogen-go/ogen/internal/jsonpointer"
 	"github.com/ogen-go/ogen/internal/location"
 	"github.com/ogen-go/ogen/internal/testutil"
+	"github.com/ogen-go/ogen/jsonschema"
+	"github.com/ogen-go/ogen/openapi"
 	"github.com/ogen-go/ogen/openapi/parser"
 )
 
@@ -47,3 +55,165 @@ func TestNegative(t *testing.T) {
 		t.Log(pretty)
 	})
 }
+
+func TestParserDeep(t *testing.T) {
+	tests := []struct {
+		name            string
+		dir             string
+		file            string
+		expected        func() openapi.API
+		expectedSpecErr string
+		expectedAPIErr  string
+	}{
+		{
+			name: "pet-tags",
+			dir:  "_testdata/remotes/api",
+			file: "pet-tags.yml",
+			expected: func() openapi.API {
+				petSchema := &jsonschema.Schema{
+					Ref: jsonpointer.RefKey{
+						Loc: "jsonschema://dummy",
+						Ptr: "#/components/schemas/Pet",
+					},
+					Type: "object",
+					Properties: []jsonschema.Property{
+						{
+							Name: "id",
+							Schema: &jsonschema.Schema{
+								Type:   "integer",
+								Format: "int64",
+								ExtraTags: map[string]string{
+									"gorm":  "primaryKey",
+									"valid": "customIdValidator",
+								},
+							},
+							Required: true,
+						},
+						{
+							Name: "name",
+							Schema: &jsonschema.Schema{
+								Type: "string",
+								ExtraTags: map[string]string{
+									"valid": "customNameValidator",
+								},
+							},
+							Required: true,
+						},
+						{
+							Name: "tag",
+							Schema: &jsonschema.Schema{
+								Type: "string",
+							},
+						},
+					},
+				}
+
+				return openapi.API{
+					Components: &openapi.Components{
+						Schemas: map[string]*jsonschema.Schema{
+							"Pet": petSchema,
+						},
+					},
+				}
+			},
+		},
+	}
+	_ = tests
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(fmt.Sprintf("Test %s", tt.name), func(t *testing.T) {
+			a := require.New(t)
+
+			filePath := path.Join(tt.dir, tt.file)
+			fileData, fileErr := testdata.ReadFile(filePath)
+			a.NoErrorf(fileErr, "load file %s", filePath)
+			fileLocation := location.NewFile(tt.file, filePath, fileData)
+
+			spec, specErr := ogen.Parse(fileData)
+			a.NoErrorf(specErr, "parse file")
+
+			var external jsonschema.ExternalResolver
+			api, apiErr := parser.Parse(spec, parser.Settings{
+				External:   external,
+				File:       fileLocation,
+				RootURL:    nil,
+				InferTypes: false,
+			})
+			var apiErrText string
+			if apiErr != nil {
+				apiErrText = apiErr.Error()
+			}
+			a.Equal(tt.expectedAPIErr, apiErrText, "err")
+
+			expected := tt.expected()
+			for k, s := range expected.Components.Schemas {
+				if aSchema, has := api.Components.Schemas[k]; !has {
+					continue
+				} else {
+					s.Locator = aSchema.Locator
+					s.Source = aSchema.Source
+					pMax := len(aSchema.Properties)
+					if pMax > len(s.Properties) {
+						pMax = len(s.Properties)
+					}
+					for p := 0; p < pMax; p++ {
+						propActual := aSchema.Properties[p]
+						s.Properties[p].Schema.Locator = propActual.Schema.Locator
+						s.Properties[p].Schema.Source = propActual.Schema.Source
+					}
+				}
+			}
+
+			checkDeep(t, "Components.Schemas", expected.Components.Schemas, api.Components.Schemas)
+		})
+	}
+}
+
+func checkDeep(t *testing.T, part string, expected, actual interface{}) {
+	t.Helper()
+
+	var err error
+	var expectedY []byte
+	if expected != nil {
+		expectedY, err = yaml.Marshal(expected)
+		require.NoError(t, err, part)
+	}
+	var actualY []byte
+	if actual != nil {
+		actualY, err = yaml.Marshal(actual)
+		require.NoError(t, err, part)
+	}
+
+	// from github.com/stretchr/testify/assert/assertions.go:diff()
+	var spewConfig = spew.ConfigState{
+		Indent:                  " ",
+		DisablePointerAddresses: true,
+		DisableCapacities:       true,
+		SortKeys:                true,
+		DisableMethods:          true,
+		MaxDepth:                10,
+	}
+	var spewConfigStringerEnabled = spew.ConfigState{
+		Indent:                  " ",
+		DisablePointerAddresses: true,
+		DisableCapacities:       true,
+		SortKeys:                true,
+		MaxDepth:                10,
+	}
+	var e, a string
+
+	switch reflect.TypeOf(expected) {
+	case reflect.TypeOf(""):
+		e = reflect.ValueOf(expected).String()
+		a = reflect.ValueOf(actual).String()
+	case reflect.TypeOf(time.Time{}):
+		e = spewConfigStringerEnabled.Sdump(expected)
+		a = spewConfigStringerEnabled.Sdump(actual)
+	default:
+		e = spewConfig.Sdump(expected)
+		a = spewConfig.Sdump(actual)
+	}
+
+	require.Equal(t, expected, actual, fmt.Sprintf("%s\nEXPECTED:\n\n%s\n%s\nACTUAL:\n\n%s\n%s", part, expectedY, e, actualY, a))
+}