OpenAPITools / OpenAPITools/openapi-generator
[BUG][Go] oneOf with enum and regex string matches more than one schema
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 26.8k
- Forks
- 7.7k
- PR merge metrics
- PR metrics pending
Description
Bug Report Checklist
- Have you provided a full/minimal spec to reproduce the issue?
- Have you validated the input using an OpenAPI validator?
- Have you tested with the latest master to confirm the issue still exists?
- Have you searched for related issues/PRs?
- What's the actual output vs expected output?
- [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description
When generating the Go Client, the generated UnmarshalJSON matches more than one schema in an oneOf, when the oneOf contains an enum and a string with regex.
The regex isn't used here as validation, so every string which matches with the enum, also matches with the string and results then in an error.
Generated model for the oneOf, where it doesn't contains any regex validation
/*
OneOf enum and string parsing fails example
No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
API version: 0.0.1
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package openapi
import (
"encoding/json"
"fmt"
"gopkg.in/validator.v2"
)
// EnumOrUUID - The identifier (ID) of an area.
type EnumOrUUID struct {
Enum *Enum
String *string
}
// EnumAsEnumOrUUID is a convenience function that returns Enum wrapped in EnumOrUUID
func EnumAsEnumOrUUID(v *Enum) EnumOrUUID {
return EnumOrUUID{
Enum: v,
}
}
// stringAsEnumOrUUID is a convenience function that returns string wrapped in EnumOrUUID
func StringAsEnumOrUUID(v *string) EnumOrUUID {
return EnumOrUUID{
String: v,
}
}
// Unmarshal JSON data into one of the pointers in the struct
func (dst *EnumOrUUID) UnmarshalJSON(data []byte) error {
var err error
match := 0
// try to unmarshal data into Enum
err = newStrictDecoder(data).Decode(&dst.Enum)
if err == nil {
jsonEnum, err := json.Marshal(dst.Enum)
if string(jsonEnum) == "{}" { // empty struct
dst.Enum = nil
} else {
if err = validator.Validate(dst.Enum); err != nil {
dst.Enum = nil
} else {
match++
}
}
} else {
dst.Enum = nil
}
// try to unmarshal data into String
err = newStrictDecoder(data).Decode(&dst.String)
if err == nil {
jsonString, _ := json.Marshal(dst.String)
if string(jsonString) == "{}" { // empty struct
dst.String = nil
} else {
if err = validator.Validate(dst.String); err != nil {
dst.String = nil
} else {
match++
}
}
} else {
dst.String = nil
}
if match > 1 { // more than 1 match
// reset to nil
dst.Enum = nil
dst.String = nil
return fmt.Errorf("data matches more than one schema in oneOf(EnumOrUUID)")
} else if match == 1 {
return nil // exactly one match
} else { // no match
return fmt.Errorf("data failed to match schemas in oneOf(EnumOrUUID)")
}
}
// Marshal data from the first non-nil pointers in the struct to JSON
func (src EnumOrUUID) MarshalJSON() ([]byte, error) {
if src.Enum != nil {
return json.Marshal(&src.Enum)
}
if src.String != nil {
return json.Marshal(&src.String)
}
return nil, nil // no data in oneOf schemas
}
// Get the actual instance
func (obj *EnumOrUUID) GetActualInstance() (interface{}) {
if obj == nil {
return nil
}
if obj.Enum != nil {
return obj.Enum
}
if obj.String != nil {
return obj.String
}
// all schemas are nil
return nil
}
// Get the actual instance value
func (obj EnumOrUUID) GetActualInstanceValue() (interface{}) {
if obj.Enum != nil {
return *obj.Enum
}
if obj.String != nil {
return *obj.String
}
// all schemas are nil
return nil
}
type NullableEnumOrUUID struct {
value *EnumOrUUID
isSet bool
}
func (v NullableEnumOrUUID) Get() *EnumOrUUID {
return v.value
}
func (v *NullableEnumOrUUID) Set(val *EnumOrUUID) {
v.value = val
v.isSet = true
}
func (v NullableEnumOrUUID) IsSet() bool {
return v.isSet
}
func (v *NullableEnumOrUUID) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableEnumOrUUID(val *EnumOrUUID) *NullableEnumOrUUID {
return &NullableEnumOrUUID{value: val, isSet: true}
}
func (v NullableEnumOrUUID) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableEnumOrUUID) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}
openapi-generator version
7.14
OpenAPI declaration file content or url
openapi: 3.0.1
info:
title: OneOf enum and string parsing fails example
version: 0.0.1
paths:
/v1/projects:
get:
summary: Find all projects.
operationId: findAllProjects
responses:
"200":
content:
application/json:
schema:
$ref: '#/components/schemas/Project'
description: OK.
servers:
- url: "https://localhost:8080/api"
description: The localhost API server
components:
schemas:
EnumOrUUID:
description: Enum or uuid field.
oneOf:
- $ref: '#/components/schemas/UUID'
- $ref: '#/components/schemas/Enum'
Project:
description: Object that represents a project.
properties:
enumOrUUID:
$ref: '#/components/schemas/EnumOrUUID'
type: object
UUID:
description: UUID
format: uuid
maxLength: 36
minLength: 36
pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
type: string
Enum:
description: Enum
enum:
- PUBLIC
- PRIVATE
example: PUBLIC
type: string
Generation Details
$ openapi-generator generate -i openapi.yml -g go -o ./tmp
no additional configs
Steps to reproduce
- Generate the Go API client based on openapi spec above
$ openapi-generator generate -i openapi.yml -g go -o ./tmp - Add this testfile in ./tmp/model_enum_or_uuid_test.go
package openapi
import "testing"
func TestEnumOrUUID_UnmarshalJSON(t *testing.T) {
type fields struct {
Enum *Enum
String *string
}
type args struct {
data []byte
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
{
// fails
name: "enum",
fields: fields{
Enum: PUBLIC.Ptr(),
},
args: args{
data: []byte(`"PUBLIC"`),
},
wantErr: false,
},
{
// works
name: "string",
fields: fields{
String: PtrString("44948b79-fb3d-47ca-85d8-fb64ea65436d"),
},
args: args{
data: []byte(`"44948b79-fb3d-47ca-85d8-fb64ea65436d"`),
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dst := &EnumOrUUID{
Enum: tt.fields.Enum,
String: tt.fields.String,
}
if err := dst.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
t.Errorf("EnumOrUUID.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
- Run the test
$ go test
Related issues/PRs
Suggest a fix
The string should be validated against the regex, to ensure that only valid strings are assigned to the attribute.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with openapi.yml and the generated model_enum_or_uuid_test.go; reproduce the issue using the documented openapi-generator generate command, then inspect EnumOrUUID.UnmarshalJSON. Done means enum values no longer produce multiple-match errors, UUID strings are checked against the declared pattern, and go test passes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100