Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] Components marked nullable in swagger file are not nullable in generated output #522

Closed
point-source opened this issue Dec 17, 2022 · 1 comment · Fixed by #525
Closed
Assignees
Labels
bug Something isn't working Triage needed

Comments

@point-source
Copy link

point-source commented Dec 17, 2022

Describe the bug
I had originally created this ticket to document this issue which was coming from two separate generation errors. The original ticket was closed as completed once one of the two errors was resolved but one still remains. I am opening this specifically for the remaining case.

In the case where there are two swagger components, one referencing another and where the referenced component is marked nullable, the generated code will not have made that class field nullable even though it ought to be.

Here is an example of a component from this spec which references another component called PlaidError.

This component is called Item and can be found on line 43182:

            "Item": {
                "description": "Metadata about the Item.",
                "type": "object",
                "additionalProperties": true,
                "properties": {
                    "item_id": {
                        "description": "The Plaid Item ID. The `item_id` is always unique; linking the same account at the same institution twice will result in two Items with different `item_id` values. Like all Plaid identifiers, the `item_id` is case-sensitive.",
                        "type": "string"
                    },
                    "institution_id": {
                        "description": "The Plaid Institution ID associated with the Item. Field is `null` for Items created via Same Day Micro-deposits.",
                        "type": "string",
                        "nullable": true
                    },
                    "webhook": {
                        "description": "The URL registered to receive webhooks for the Item.",
                        "type": "string",
                        "nullable": true
                    },
                    "error": {
                        "$ref": "#/components/schemas/PlaidError"
                    },
                    "available_products": {
                        "description": "A list of products available for the Item that have not yet been accessed. The contents of this array will be mutually exclusive with `billed_products`.",
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Products"
                        }
                    },
                    "billed_products": {
                        "description": "A list of products that have been billed for the Item. The contents of this array will be mutually exclusive with `available_products`. Note - `billed_products` is populated in all environments but only requests in Production are billed. Also note that products that are billed on a pay-per-call basis rather than a pay-per-Item basis, such as `balance`, will not appear here.\n",
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Products"
                        }
                    },
                    "products": {
                        "description": "A list of authorized products for the Item.\n",
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Products"
                        }
                    },
                    "consented_products": {
                        "description": "Beta: A list of products that have gone through consent collection for the Item. Only present for those enabled in the beta.\n",
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Products"
                        }
                    },
                    "consent_expiration_time": {
                        "description": "The RFC 3339 timestamp after which the consent provided by the end user will expire. Upon consent expiration, the item will enter the `ITEM_LOGIN_REQUIRED` error state. To circumvent the `ITEM_LOGIN_REQUIRED` error and maintain continuous consent, the end user can reauthenticate via Link’s update mode in advance of the consent expiration time.\n\nNote - This is only relevant for certain OAuth-based institutions. For all other institutions, this field will be null.\n",
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "update_type": {
                        "type": "string",
                        "description": "Indicates whether an Item requires user interaction to be updated, which can be the case for Items with some forms of two-factor authentication.\n\n`background` - Item can be updated in the background\n\n`user_present_required` - Item requires user interaction to be updated",
                        "enum": [
                            "background",
                            "user_present_required"
                        ]
                    }
                },
                "required": [
                    "item_id",
                    "webhook",
                    "error",
                    "available_products",
                    "billed_products",
                    "consent_expiration_time",
                    "update_type"
                ]
            },

Notice the reference in the error property of the above component (line 43201 of the swagger file):

"error": {
                        "$ref": "#/components/schemas/PlaidError"
                    },

If we follow the reference to the appropriate component definition, we find this on line 18570:

            "PlaidError": {
                "description": "We use standard HTTP response codes for success and failure notifications, and our errors are further classified by `error_type`. In general, 200 HTTP codes correspond to success, 40X codes are for developer- or user-related failures, and 50X codes are for Plaid-related issues. An Item with a non-`null` error object will only be part of an API response when calling `/item/get` to view Item status. Otherwise, error fields will be `null` if no error has occurred; if an error has occurred, an error code will be returned instead.",
                "type": "object",
                "additionalProperties": true,
                "title": "Error",
                "nullable": true,
                "properties": {
                    "error_type": {
                        "$ref": "#/components/schemas/PlaidErrorType"
                    },
                    "error_code": {
                        "description": "The particular error code. Safe for programmatic use.",
                        "type": "string"
                    },
                    "error_message": {
                        "description": "A developer-friendly representation of the error code. This may change over time and is not safe for programmatic use.",
                        "type": "string"
                    },
                    "display_message": {
                        "description": "A user-friendly representation of the error code. `null` if the error is not related to user action.\n\nThis may change over time and is not safe for programmatic use.",
                        "type": "string",
                        "nullable": true
                    },
                    "request_id": {
                        "type": "string",
                        "description": "A unique ID identifying the request, to be used for troubleshooting purposes. This field will be omitted in errors provided by webhooks."
                    },
                    "causes": {
                        "type": "array",
                        "description": "In the Assets product, a request can pertain to more than one Item. If an error is returned for such a request, `causes` will return an array of errors containing a breakdown of these errors on the individual Item level, if any can be identified.\n\n`causes` will only be provided for the `error_type` `ASSET_REPORT_ERROR`. `causes` will also not be populated inside an error nested within a `warning` object.",
                        "items": {}
                    },
                    "status": {
                        "type": "number",
                        "description": "The HTTP status code associated with the error. This will only be returned in the response body when the error information is provided via a webhook.",
                        "nullable": true
                    },
                    "documentation_url": {
                        "type": "string",
                        "description": "The URL of a Plaid documentation page with more information about the error"
                    },
                    "suggested_action": {
                        "type": "string",
                        "nullable": true,
                        "description": "Suggested steps for resolving the error"
                    }
                },
                "required": [
                    "error_type",
                    "error_code",
                    "error_message",
                    "display_message"
                ]
            },

Notice that line 18575 specifies the entire PlaidError component as nullable on any component that references it:

                "nullable": true,

Yet when we generate code for the Item component, it looks like this:

@JsonSerializable(explicitToJson: true)
class Item {
  Item({
    required this.itemId,
    this.institutionId,
    required this.webhook,
    required this.error,
    required this.availableProducts,
    required this.billedProducts,
    this.products,
    this.consentedProducts,
    required this.consentExpirationTime,
    required this.updateType,
  });

  factory Item.fromJson(Map<String, dynamic> json) => _$ItemFromJson(json);

  @JsonKey(name: 'item_id')
  final String itemId;
  @JsonKey(name: 'institution_id')
  final String? institutionId;
  @JsonKey(name: 'webhook')
  final String? webhook;
  @JsonKey(name: 'error')
  final PlaidError error;
  @JsonKey(
    name: 'available_products',
    toJson: productsListToJson,
    fromJson: productsListFromJson,
  )
  final List<enums.Products> availableProducts;
  @JsonKey(
    name: 'billed_products',
    toJson: productsListToJson,
    fromJson: productsListFromJson,
  )
  final List<enums.Products> billedProducts;
  @JsonKey(
    name: 'products',
    toJson: productsListToJson,
    fromJson: productsListFromJson,
  )
  final List<enums.Products>? products;
  @JsonKey(
    name: 'consented_products',
    toJson: productsListToJson,
    fromJson: productsListFromJson,
  )
  final List<enums.Products>? consentedProducts;
  @JsonKey(name: 'consent_expiration_time')
  final DateTime? consentExpirationTime;
  @JsonKey(
    name: 'update_type',
    toJson: itemUpdateTypeToJson,
    fromJson: itemUpdateTypeFromJson,
  )
  final enums.ItemUpdateType updateType;
  static const fromJsonFactory = _$ItemFromJson;
  static const toJsonFactory = _$ItemToJson;
  Map<String, dynamic> toJson() => _$ItemToJson(this);

  @override
  bool operator ==(dynamic other) {
    return identical(this, other) ||
        (other is Item &&
            (identical(other.itemId, itemId) ||
                const DeepCollectionEquality().equals(other.itemId, itemId)) &&
            (identical(other.institutionId, institutionId) ||
                const DeepCollectionEquality()
                    .equals(other.institutionId, institutionId)) &&
            (identical(other.webhook, webhook) ||
                const DeepCollectionEquality()
                    .equals(other.webhook, webhook)) &&
            (identical(other.error, error) ||
                const DeepCollectionEquality().equals(other.error, error)) &&
            (identical(other.availableProducts, availableProducts) ||
                const DeepCollectionEquality()
                    .equals(other.availableProducts, availableProducts)) &&
            (identical(other.billedProducts, billedProducts) ||
                const DeepCollectionEquality()
                    .equals(other.billedProducts, billedProducts)) &&
            (identical(other.products, products) ||
                const DeepCollectionEquality()
                    .equals(other.products, products)) &&
            (identical(other.consentedProducts, consentedProducts) ||
                const DeepCollectionEquality()
                    .equals(other.consentedProducts, consentedProducts)) &&
            (identical(other.consentExpirationTime, consentExpirationTime) ||
                const DeepCollectionEquality().equals(
                    other.consentExpirationTime, consentExpirationTime)) &&
            (identical(other.updateType, updateType) ||
                const DeepCollectionEquality()
                    .equals(other.updateType, updateType)));
  }

  @override
  String toString() => jsonEncode(this);

  @override
  int get hashCode =>
      const DeepCollectionEquality().hash(itemId) ^
      const DeepCollectionEquality().hash(institutionId) ^
      const DeepCollectionEquality().hash(webhook) ^
      const DeepCollectionEquality().hash(error) ^
      const DeepCollectionEquality().hash(availableProducts) ^
      const DeepCollectionEquality().hash(billedProducts) ^
      const DeepCollectionEquality().hash(products) ^
      const DeepCollectionEquality().hash(consentedProducts) ^
      const DeepCollectionEquality().hash(consentExpirationTime) ^
      const DeepCollectionEquality().hash(updateType) ^
      runtimeType.hashCode;
}

Notice this line is not nullable even though it should be:

  @JsonKey(name: 'error')
  final PlaidError error;

This would be the correct generated output:

  @JsonKey(name: 'error')
  final PlaidError? error;

To Reproduce

  1. Clone this repo: https://github.com/point-source/dart_plaid
  2. Re-run generation
  3. Observe that the Item model contains a non-nullable PlaidError field/

Expected behavior
The PlaidError component should be nullable in all generated code since it is marked as nullable in the swagger and this should force it to be nullable anywhere it is referenced.

Swagger specification link
https://raw.githubusercontent.com/point-source/dart_plaid/main/swagger/plaid_service.swagger

Library version used:
2.9.0

Additional context
This was originally documented here: #506

@point-source point-source added bug Something isn't working Triage needed labels Dec 17, 2022
@point-source point-source changed the title [BUG] Properties marked nullable in swagger file are not nullable in generated output (Part 2) [BUG] Components marked nullable in swagger file are not nullable in generated output Dec 17, 2022
@point-source
Copy link
Author

Awesome! Confirmed this is working. Thanks for the quick fix!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working Triage needed
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants