OpenAPITools / openapi-generator

OpenAPI Generator allows generation of API client libraries (SDK generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (v2, v3)
https://openapi-generator.tech
Apache License 2.0
21.86k stars 6.59k forks source link

[openapi spec 3.1.0][python-flask generator] Nullable properties not propagated to child models #19619

Open a-akshat opened 1 month ago

a-akshat commented 1 month ago
Description

Currently, we use openapi spec 3.0.3 with openapi-generator version 5.4.0 and we are planning to migrate to openapi spec 3.1.0 with the latest generator version.

While migrating to 3.1.0 I see that nullable is not supported anymore and we use the isNullable value to set our generated model instance variable to Optional as follows:

{{#isNullable}}Optional[{{/isNullable}}{{#isDateTime}}Union[datetime, DateTimeProxy]{{/isDateTime}}{{^isDateTime}}{{dataType}}{{/isDateTime}}{{#isNullable}}]{{/isNullable}}

I noticed that with the new version of the generator and openapi spec 3.1.0 when we set a property as "null" this constraint is not inherited by the child schema in the model generated. Here is a simple example:

Openapi spec 3.1.0:

openapi: 3.1.0
paths:
  /users/{userId}:
    get:
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserResponse'

components:
  schemas:
    BaseUser:
      type: object
      properties:
        id:
          type:
            - string
            - 'null'
        username:
          type: string

    UserResponse:
      type: object
      allOf:
        - $ref: '#/components/schemas/BaseUser'
        - type: object
          properties:
            email:
              type: string
              description: The email of the user

Model generated: base_user.py

class BaseUser(TypedDict):
    id: Optional[str]
    username: str

user_response.py

class UserResponse(TypedDict):
    id: str
    username: str
    email: str

Notice how the id : Optional[str]is not the same as parent in the child schema. This worked as expected with 3.0.3 and the new version of the generator as well. So makes me wonder if the 3.1.0 spec isNullable property is propagated to the child schema.

Please let me know if there is a way I can resolve this as we want to utilize the great features in 3.1.0 without breaking our client code.

openapi-generator version

7.8.0

OpenAPI declaration file content or url
Command line used for generation

openapi-generator-cli generate --input-spec spec/bundle.yaml --config openapi-generator.yaml --output ../..

Note: the openapi-generator.yaml config file is straightforward and no options are enabled in it.

wing328 commented 1 month ago

i tested with python-flask in the latest master and don't see TypedDict in the auto-generated models so looks like you're using a customized version/template somehow

i also tested with python client generator and couldn't repeat the issue, which means id is correctly annotated with the type Optional[str]

a-akshat commented 1 month ago

@wing328 yes you are right, we use a customized templates for our use case. Also, please note that this template worked with openapi spec 3.0.3 and latest version of the generator Here is the model customized template that produces the TypedDict

{{/parent}}
{{! Simple object types get an Enum- or TypedDict-base representation: }}
{{^parent}}
{{#isEnum}}
class {{classname}}({{#isString}}str, {{/isString}}enum.Enum):
{{#allowableValues}}
    {{#enumVars}}
    {{name}} = {{{value}}}
    {{/enumVars}}
{{/allowableValues}}
{{/isEnum}}
{{^isEnum}}
class {{classname}}(TypedDict):
{{#vars}}
    {{name}}: {{> field_type}}
{{/vars}}
{{/isEnum}}
{{/parent}}
{{/vendorExtensions.x-python-imports}}
{{/model}}
{{/models}}

field_type.mustache: {{#isNullable}}Optional[{{/isNullable}}{{#isDateTime}}Union[datetime, DateTimeProxy]{{/isDateTime}}{{^isDateTime}}{{dataType}}{{/isDateTime}}{{#isNullable}}]{{/isNullable}}

Do you recommend using the python generator instead of python flask for the models? https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator/src/main/resources/python/model_generic.mustache

wing328 commented 1 month ago
    "vars" : [ {
      "openApiType" : "string",
      "baseName" : "id",
      "getter" : "getId",
      "setter" : "setId",
      "dataType" : "str",
      "datatypeWithEnum" : "str",
      "name" : "id",
      "defaultValueWithParam" : " = data.id;",
      "baseType" : "str",
      "example" : "''",
      "jsonSchema" : "{\r\n  \"nullable\" : true,\r\n  \"type\" : \"string\"\r\n}",
      "exclusiveMinimum" : false,
      "exclusiveMaximum" : false,
      "required" : false,
      "deprecated" : false,
      "hasMoreNonReadOnly" : false,
      "isPrimitiveType" : true,
      "isModel" : false,
      "isContainer" : false,
      "isString" : true,
      "isNumeric" : false,
      "isInteger" : false,
      "isShort" : false,
      "isLong" : false,
      "isUnboundedInteger" : false,
      "isNumber" : false,
      "isFloat" : false,
      "isDouble" : false,
      "isDecimal" : false,
      "isByteArray" : false,
      "isBinary" : false,
      "isFile" : false,
      "isBoolean" : false,
      "isDate" : false,
      "isDateTime" : false,
      "isUuid" : false,
      "isUri" : false,
      "isEmail" : false,
      "isPassword" : false,
      "isNull" : false,
      "isVoid" : false,
      "isFreeFormObject" : false,
      "isAnyType" : false,
      "isArray" : false,
      "isMap" : false,
      "isOptional" : false,
      "isEnum" : false,
      "isInnerEnum" : false,
      "isEnumRef" : false,
      "isReadOnly" : false,
      "isWriteOnly" : false,
      "isNullable" : true,

using --global-property debugModels in CLI, isNullable is correctly set to true so no idea why it's not working in your use cases with customized templates.

a-akshat commented 1 month ago

@wing328 thank you for pointing me towards the debugModels option it makes things clearer. I can confirm that isNullable is not set on the UserResponse

{
  "importPath" : "from api.v5.generated.models.user_response import UserResponse",
  "pyImports" : [ ],
  "model" : {
    "interfaces" : [ "BaseUser" ],
    "anyOf" : [ ],
    "oneOf" : [ ],
    "allOf" : [ "BaseUser" ],
    "name" : "UserResponse",
    **"schemaName" : "UserResponse",
    "classname" : "UserResponse",**
    "classVarName" : "user_response",
    "modelJson" : "{\n  \"allOf\" : [ {\n    \"$ref\" : \"#/components/schemas/BaseUser\"\n  }, {\n    \"properties\" : {\n      \"email\" : {\n        \"type\" : \"string\",\n        \"description\" : \"The email of the user\"\n      }\n    }\n  } ]\n}",
    "dataType" : "BaseUser",
    "classFilename" : "user_response",
    "isAlias" : false,
    "isString" : false,
    "isInteger" : false,
    "isLong" : false,
    "isNumber" : false,
    "isNumeric" : false,
    "isFloat" : false,
    "isDouble" : false,
    "isDate" : false,
    "isDateTime" : false,
    "isDecimal" : false,
    "isShort" : false,
    "isUnboundedInteger" : false,
    "isPrimitiveType" : false,
    "isBoolean" : false,
    "isFreeFormObject" : false,
    "additionalPropertiesIsAnyType" : false,
    "vars" : [ {
      "openApiType" : "string",
      "baseName" : "id",
      "getter" : "getId",
      "setter" : "setId",
      "dataType" : "str",
      "datatypeWithEnum" : "str",
      "name" : "id",
      "defaultValueWithParam" : " = data.id;",
      "baseType" : "str",
      "title" : "id",
      "example" : "''",
      "jsonSchema" : "{\n  \"title\" : \"id\"\n}",
      "exclusiveMinimum" : false,
      "exclusiveMaximum" : false,
      "required" : false,
      "deprecated" : false,
      "hasMoreNonReadOnly" : false,
      "isPrimitiveType" : true,
      "isModel" : false,
      "isContainer" : false,
      "isString" : true,
      "isNumeric" : false,
      "isInteger" : false,
      "isShort" : false,
      "isLong" : false,
      "isUnboundedInteger" : false,
      "isNumber" : false,
      "isFloat" : false,
      "isDouble" : false,
      "isDecimal" : false,
      "isByteArray" : false,
      "isBinary" : false,
      "isFile" : false,
      "isBoolean" : false,
      "isDate" : false,
      "isDateTime" : false,
      "isUuid" : false,
      "isUri" : false,
      "isEmail" : false,
      "isPassword" : false,
      "isNull" : false,
      "isVoid" : false,
      "isFreeFormObject" : false,
      "isAnyType" : false,
      "isArray" : false,
      "isMap" : false,
      "isOptional" : false,
      "isEnum" : false,
      "isInnerEnum" : false,
      "isEnumRef" : false,
      "isReadOnly" : false,
      "isWriteOnly" : false,
      **"isNullable" : false,**
      "isSelfReference" : false,
      "isCircularReference" : false,
      "isDiscriminator" : false,
      "isNew" : false,
      "isOverridden" : false,
      "vars" : [ ],
      "requiredVars" : [ ],
      "vendorExtensions" : {
        "x-py-typing" : "Optional[StrictStr] = None"
      },
      "hasValidation" : false,
      "isInherited" : false,
      "nameInCamelCase" : "id",
      "nameInPascalCase" : "Id",
      "nameInSnakeCase" : "ID",
      "uniqueItems" : false,
      "isXmlAttribute" : false,
      "isXmlWrapped" : false,
      "additionalPropertiesIsAnyType" : false,
      "hasVars" : false,
      "hasRequired" : false,
      "hasDiscriminatorWithNonEmptyMapping" : false,
      "hasMultipleTypes" : false,
      "schemaIsFromAdditionalProperties" : false,
      "isBooleanSchemaTrue" : false,
      "isBooleanSchemaFalse" : false,
      "isEnumOrRef" : false,
      "datatype" : "str",
      "exclusiveMaximum" : false,
      "hasItems" : false
    }

Is it possible that you might be looking at BaseUser ?

a-akshat commented 1 month ago

Btw, I found a workaround, there is a vendor extension that can be used to mark a property nullable. x-nullable: true propagates isNullable as true to its children. Let me know if this is not the intended use-case.