Carapacik / swagger_parser

Dart package that takes an OpenApi definition file and generates REST clients based on retrofit and data classes for your project.
https://pub.dev/packages/swagger_parser
MIT License
100 stars 47 forks source link

Not supported specify nullable types via anyOf #84

Closed StarProxima closed 1 year ago

StarProxima commented 1 year ago

The new pydantic 2.1 on backend (Python, FastApi) describes nullable types in the anyOf specification. It would be nice to support this way of specifying a nullable type.

Small example:

 "last_name": {
    "anyOf": [
      {
        "type": "string"
      },
      {
        "type": "null"
      }
    ],
    "title": "Last Name"
 }

Result:

import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.freezed.dart';
part 'user.g.dart';

@Freezed()
class User with _$User {
  const factory User({
    /// Адрес электронной почты
    required String email,
    /// Имя пользователя
    @JsonKey(name: 'first_name')
    required String firstName,
    /// Фамилия пользователя
    @JsonKey(name: 'last_name')
    required Object lastName,
    /// Никнейм пользователя
    required String username,
    /// Уникальный id пользователя в базе данных
    required int id,
    /// Уникальный uuid пользователя в базе данных
    required String uuid,
    /// Время создания аккаунта пользователя
    @JsonKey(name: 'created_at')
    required DateTime createdAt,
    /// Время удаления аккаунта пользователя
    @JsonKey(name: 'deleted_at')
    required Object deletedAt,
    /// Время изменения аккаунта пользователя, либо обновления refresh токена
    @JsonKey(name: 'updated_at')
    required Object updatedAt,
  }) = _User;

  factory User.fromJson(Map<String, Object?> json) => _$UserFromJson(json);
}

Expected result:

import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.freezed.dart';
part 'user.g.dart';

@Freezed()
class User with _$User {
  const factory User({
    /// Адрес электронной почты
    required String email,
    /// Имя пользователя
    @JsonKey(name: 'first_name')
    required String firstName,
    /// Фамилия пользователя
    @JsonKey(name: 'last_name')
    required String? lastName,
    /// Никнейм пользователя
    required String username,
    /// Уникальный id пользователя в базе данных
    required int id,
    /// Уникальный uuid пользователя в базе данных
    required String uuid,
    /// Время создания аккаунта пользователя
    @JsonKey(name: 'created_at')
    required DateTime createdAt,
    /// Время удаления аккаунта пользователя
    @JsonKey(name: 'deleted_at')
    required DateTime? deletedAt,
    /// Время изменения аккаунта пользователя, либо обновления refresh токена
    @JsonKey(name: 'updated_at')
    required DateTime? updatedAt,
  }) = _User;

  factory User.fromJson(Map<String, Object?> json) => _$UserFromJson(json);
}

OpenApi:

{
  "openapi": "3.1.0",
  "info": {
    "title": "Microservice Auth API methods",
    "version": "0.2.2"
  },
  "paths": {},
  "components": {
    "schemas": {
      "User": {
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "title": "Email",
            "description": "Адрес электронной почты"
          },
          "first_name": {
            "type": "string",
            "title": "First Name",
            "description": "Имя пользователя"
          },
          "last_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Name",
            "description": "Фамилия пользователя"
          },
          "username": {
            "type": "string",
            "title": "Username",
            "description": "Никнейм пользователя"
          },
          "id": {
            "type": "integer",
            "title": "Id",
            "description": "Уникальный id пользователя в базе данных"
          },
          "uuid": {
            "type": "string",
            "format": "uuid",
            "title": "Uuid",
            "description": "Уникальный uuid пользователя в базе данных"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At",
            "description": "Время создания аккаунта пользователя"
          },
          "deleted_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Deleted At",
            "description": "Время удаления аккаунта пользователя"
          },
          "updated_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Updated At",
            "description": "Время изменения аккаунта пользователя, либо обновления refresh токена"
          }
        },
        "type": "object",
        "required": [
          "email",
          "first_name",
          "last_name",
          "username",
          "id",
          "uuid",
          "created_at",
          "deleted_at",
          "updated_at"
        ],
        "title": "User"
      }
    },
    "tags": [
      {
        "name": "Auth",
        "description": "Auth Management"
      }
    ]
  }
}
Carapacik commented 1 year ago

Is there any annotation in your backend so that nullable is used?

"last_name": {
    "type": "string",
    "nullable": true,
    "title": "Last Name"
 }
StarProxima commented 1 year ago

Is there any annotation in your backend so that nullable is used?

"last_name": {
    "type": "string",
    "nullable": true,
    "title": "Last Name"
 }

After talking to the backend team, there is an option to add nullable, but the type will also be set via anyOf:

 "last_name": {
    "anyOf": [
      {
        "type": "string"
      },
      {
        "type": "null"
      }
    ],
    "nullable": true,
    "title": "Last Name"
 }

So far, the only solution to the problem seems to me to support setting nullable type via anyOf.

StarProxima commented 1 year ago

@Carapacik This feature looks more difficult than adding a flag. I think it is better to be implemented by a maintainer, because you need a good understanding of the package structure. What do you think about it?

Carapacik commented 1 year ago

Already think about that in https://github.com/Carapacik/swagger_parser/issues/5

StarProxima commented 1 year ago

Found a workaround using a self-describing class on the backend:

Annotated[Union[UUID | SkipJsonSchema[None]], Field(json_schema_extra=lambda x: x.pop('default', None))]