From 37461d4c9758b8fb3db58f62dd6447c4ffd53f02 Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Tue, 12 Aug 2025 17:20:53 +0200 Subject: [PATCH 1/4] fix: oneOfs with enums and strings can not be parsed --- templates/go/custom/model_oneof_test.mustache | 54 +++++++++++++++++++ templates/go/custom/model_test.mustache | 3 ++ templates/go/model_oneof.mustache | 21 ++++++-- 3 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 templates/go/custom/model_oneof_test.mustache diff --git a/templates/go/custom/model_oneof_test.mustache b/templates/go/custom/model_oneof_test.mustache new file mode 100644 index 0000000..5b6de7d --- /dev/null +++ b/templates/go/custom/model_oneof_test.mustache @@ -0,0 +1,54 @@ +{{! NOTE: This is a custom STACKIT template which is not present in upsteam to support testing of oneOf models}} + + +{{! tests only the adjusted cases in the Generator of UnmarshalJSON}} +{{#useOneOfDiscriminatorLookup}} +{{^discriminator}} +// isOneOf + +{{#composedSchemas.oneOf}} +{{#-first}} +func Test{{{classname}}}_UnmarshalJSON(t *testing.T) { + type args struct { + src []byte + } + tests := []struct { + name string + args args + wantErr bool + }{ +{{/-first}} + {{#allowableValues.values}} + { + name: "success - {{dataType}} {{.}}", + args: args{ + src: []byte(`"{{.}}"`), + }, + wantErr: false, + }, + {{/allowableValues.values}} + {{^allowableValues.values}}{{^isModel}} + { + name: "success - {{dataType}} {{example}}", + args: args{ + src: []byte(`"{{example}}"`), + }, + wantErr: false, + }, + {{/isModel}}{{/allowableValues.values}} +{{#-last}} + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := &{{{classname}}}{} + if err := v.UnmarshalJSON(tt.args.src); (err != nil) != tt.wantErr { + t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} +{{/-last}} +{{/composedSchemas.oneOf}} + +{{/discriminator}} +{{/useOneOfDiscriminatorLookup}} \ No newline at end of file diff --git a/templates/go/custom/model_test.mustache b/templates/go/custom/model_test.mustache index 65e6577..7a4ee37 100644 --- a/templates/go/custom/model_test.mustache +++ b/templates/go/custom/model_test.mustache @@ -12,6 +12,9 @@ import ( {{#model}} {{^isEnum}} +{{#oneOf}} +{{#-first}}{{>custom/model_oneof_test}}{{/-first}} +{{/oneOf}} {{^oneOf}}{{^anyOf}} {{>custom/model_simple_test}} {{/anyOf}}{{/oneOf}} diff --git a/templates/go/model_oneof.mustache b/templates/go/model_oneof.mustache index 649b03d..698f278 100644 --- a/templates/go/model_oneof.mustache +++ b/templates/go/model_oneof.mustache @@ -54,21 +54,36 @@ func (dst *{{classname}}) UnmarshalJSON(data []byte) error { {{/discriminator}} {{^discriminator}} match := 0 - {{#oneOf}} + {{! Workaround in case oneOf contains enums and strings. The enum value would match with both. }} + {{! The workaround adds a regex check for the string (in case the api spec provides a regex) }} + {{#composedSchemas.oneOf}} + {{#dataType}} // try to unmarshal data into {{#lambda.type-to-name}}{{{.}}}{{/lambda.type-to-name}} err = json.Unmarshal(data, &dst.{{#lambda.type-to-name}}{{{.}}}{{/lambda.type-to-name}}) if err == nil { json{{{.}}}, _ := json.Marshal(dst.{{#lambda.type-to-name}}{{{.}}}{{/lambda.type-to-name}}) + {{#pattern}} + regex := `{{.}}` + regex = regexp.MustCompile("^\\/|\\/$").ReplaceAllString(regex, "$1") // Remove beginning slash and ending slash + regex = regexp.MustCompile("\\\\(.)").ReplaceAllString(regex, "$1") // Remove duplicate escaping char for dots + rawString := strings.Trim(*dst.String, "\"") + {{/pattern}} if string(json{{{.}}}) == "{}" { // empty struct dst.{{#lambda.type-to-name}}{{{.}}}{{/lambda.type-to-name}} = nil - } else { + {{#pattern}} + } else if matched, _ := regexp.MatchString(regex, rawString); matched { + {{/pattern}} + {{^pattern}} + } else { + {{/pattern}} match++ } } else { dst.{{#lambda.type-to-name}}{{{.}}}{{/lambda.type-to-name}} = nil } - {{/oneOf}} + {{/dataType}} + {{/composedSchemas.oneOf}} if match > 1 { // more than 1 match // reset to nil {{#oneOf}} From ddddfb4b380ad2f47f1faabe6540f0e910fbee9d Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Wed, 13 Aug 2025 14:12:18 +0200 Subject: [PATCH 2/4] fix: set value back to nil, if it doesn't match the regex --- templates/go/model_oneof.mustache | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/templates/go/model_oneof.mustache b/templates/go/model_oneof.mustache index 698f278..dcc7997 100644 --- a/templates/go/model_oneof.mustache +++ b/templates/go/model_oneof.mustache @@ -66,16 +66,12 @@ func (dst *{{classname}}) UnmarshalJSON(data []byte) error { regex := `{{.}}` regex = regexp.MustCompile("^\\/|\\/$").ReplaceAllString(regex, "$1") // Remove beginning slash and ending slash regex = regexp.MustCompile("\\\\(.)").ReplaceAllString(regex, "$1") // Remove duplicate escaping char for dots - rawString := strings.Trim(*dst.String, "\"") + rawString := regexp.MustCompile(`^"|"$`).ReplaceAllString(*dst.String, "$1") // Remove quotes + isMatched, _ := regexp.MatchString(regex, rawString) {{/pattern}} - if string(json{{{.}}}) == "{}" { // empty struct + if string(json{{{.}}}) == "{}" {{#pattern}}|| !isMatched {{/pattern}} { // empty struct dst.{{#lambda.type-to-name}}{{{.}}}{{/lambda.type-to-name}} = nil - {{#pattern}} - } else if matched, _ := regexp.MatchString(regex, rawString); matched { - {{/pattern}} - {{^pattern}} } else { - {{/pattern}} match++ } } else { From 154a247f99e8a69cee3973c8a0a9f6f1f4cc0a4d Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Thu, 14 Aug 2025 14:31:41 +0200 Subject: [PATCH 3/4] fix: if oneOf contains multiple strings and the data doesn't match the last regex, the String attribute will be overwritten to nil --- templates/go/custom/model_oneof_test.mustache | 7 +++++++ templates/go/model_oneof.mustache | 14 ++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/templates/go/custom/model_oneof_test.mustache b/templates/go/custom/model_oneof_test.mustache index 5b6de7d..068d98e 100644 --- a/templates/go/custom/model_oneof_test.mustache +++ b/templates/go/custom/model_oneof_test.mustache @@ -44,6 +44,13 @@ func Test{{{classname}}}_UnmarshalJSON(t *testing.T) { if err := v.UnmarshalJSON(tt.args.src); (err != nil) != tt.wantErr { t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) } + marshalJson, err := v.MarshalJSON() + if err != nil { + t.Fatalf("failed marshalling {{classname}}: %v", err) + } + if string(marshalJson) != string(tt.args.src) { + t.Fatalf("wanted %s, get %s", tt.args.src, marshalJson) + } }) } } diff --git a/templates/go/model_oneof.mustache b/templates/go/model_oneof.mustache index dcc7997..0e7627d 100644 --- a/templates/go/model_oneof.mustache +++ b/templates/go/model_oneof.mustache @@ -59,23 +59,21 @@ func (dst *{{classname}}) UnmarshalJSON(data []byte) error { {{#composedSchemas.oneOf}} {{#dataType}} // try to unmarshal data into {{#lambda.type-to-name}}{{{.}}}{{/lambda.type-to-name}} - err = json.Unmarshal(data, &dst.{{#lambda.type-to-name}}{{{.}}}{{/lambda.type-to-name}}) + dst{{classname}}{{-index}} := &{{classname}}{} + err = json.Unmarshal(data, &dst{{classname}}{{-index}}.{{#lambda.type-to-name}}{{{.}}}{{/lambda.type-to-name}}) if err == nil { - json{{{.}}}, _ := json.Marshal(dst.{{#lambda.type-to-name}}{{{.}}}{{/lambda.type-to-name}}) + json{{{.}}}, _ := json.Marshal(&dst{{classname}}{{-index}}.{{#lambda.type-to-name}}{{{.}}}{{/lambda.type-to-name}}) {{#pattern}} regex := `{{.}}` regex = regexp.MustCompile("^\\/|\\/$").ReplaceAllString(regex, "$1") // Remove beginning slash and ending slash regex = regexp.MustCompile("\\\\(.)").ReplaceAllString(regex, "$1") // Remove duplicate escaping char for dots - rawString := regexp.MustCompile(`^"|"$`).ReplaceAllString(*dst.String, "$1") // Remove quotes + rawString := regexp.MustCompile(`^"|"$`).ReplaceAllString(*dst{{classname}}{{-index}}.{{#lambda.type-to-name}}{{dataType}}{{/lambda.type-to-name}}, "$1") // Remove quotes isMatched, _ := regexp.MatchString(regex, rawString) {{/pattern}} - if string(json{{{.}}}) == "{}" {{#pattern}}|| !isMatched {{/pattern}} { // empty struct - dst.{{#lambda.type-to-name}}{{{.}}}{{/lambda.type-to-name}} = nil - } else { + if string(json{{{.}}}) != "{}" {{#pattern}}&& isMatched {{/pattern}} { // empty struct + dst.{{#lambda.type-to-name}}{{{.}}}{{/lambda.type-to-name}} = dst{{classname}}{{-index}}.{{#lambda.type-to-name}}{{{.}}}{{/lambda.type-to-name}} match++ } - } else { - dst.{{#lambda.type-to-name}}{{{.}}}{{/lambda.type-to-name}} = nil } {{/dataType}} From 20e0dd5c267fdb124ddc37cd5d3820b041b6cdda Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Thu, 14 Aug 2025 15:19:11 +0200 Subject: [PATCH 4/4] add comment workaround within the template --- templates/go/model_oneof.mustache | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/templates/go/model_oneof.mustache b/templates/go/model_oneof.mustache index 0e7627d..a3474f9 100644 --- a/templates/go/model_oneof.mustache +++ b/templates/go/model_oneof.mustache @@ -54,8 +54,11 @@ func (dst *{{classname}}) UnmarshalJSON(data []byte) error { {{/discriminator}} {{^discriminator}} match := 0 - {{! Workaround in case oneOf contains enums and strings. The enum value would match with both. }} - {{! The workaround adds a regex check for the string (in case the api spec provides a regex) }} + {{! BEGIN - Workaround in case oneOf contains enums and strings. The enum value would match with both. }} + {{! This workaround adds a regex check for the string (in case the api spec provides a regex) }} + // Workaround until upstream issue is fixed: + // https://github.com/OpenAPITools/openapi-generator/issues/21751 + // Tracking issue on our side: https://jira.schwarz/browse/STACKITSDK-226 {{#composedSchemas.oneOf}} {{#dataType}} // try to unmarshal data into {{#lambda.type-to-name}}{{{.}}}{{/lambda.type-to-name}} @@ -78,6 +81,7 @@ func (dst *{{classname}}) UnmarshalJSON(data []byte) error { {{/dataType}} {{/composedSchemas.oneOf}} + {{! END - Workaround in case oneOf contains enums and strings. The enum value would match with both. }} if match > 1 { // more than 1 match // reset to nil {{#oneOf}}