OpenAPITools / OpenAPITools/openapi-generator

[BUG][Go] oneOf with enum and regex string matches more than one schema

Open
#21,751 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Issue: Bug
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
  1. Generate the Go API client based on openapi spec above
    $ openapi-generator generate -i openapi.yml -g go -o ./tmp
  2. 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)
			}
		})
	}
}
  1. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.