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.98k stars 6.6k forks source link

[BUG] v7.0.0 Elixir Deserialization Regression #16412

Open tobbbles opened 1 year ago

tobbbles commented 1 year ago

Bug Report Checklist

Description

16061 appears to have introduced a bug when attempting to deserialize a string into an enum model.

    ** (FunctionClauseError) no function clause matching in Ory.Deserializer.to_struct/2
        (ory_client 1.1.50) lib/ory/deserializer.ex:97: Ory.Deserializer.to_struct("aal1", Ory.Model.AuthenticatorAssuranceLevel)

Here we see to_struct/2 being called on the value of "aal1", to be decoded into the Ory.Model.AuthenticatorAssuranceLevel model. However there is no implementation from this.

This is a regression from the prior generators, and the expected output is to be able to deserialize into these modules/structs.

openapi-generator version

v7.0.0

OpenAPI declaration file content or url

Any string-typed enum field, for example this from the Ory SDK

      "authenticatorAssuranceLevel": {
        "description": "The authenticator assurance level can be one of \"aal1\", \"aal2\", or \"aal3\". A higher number means that it is harder\nfor an attacker to compromise the account.\n\nGenerally, \"aal1\" implies that one authentication factor was used while AAL2 implies that two factors (e.g.\npassword + TOTP) have been used.\n\nTo learn more about these levels please head over to: https://www.ory.sh/kratos/docs/concepts/credentials",
        "enum": [
          "aal0",
          "aal1",
          "aal2",
          "aal3"
        ],
        "title": "Authenticator Assurance Level (AAL)",
        "type": "string"
      },
Generation Details

The above spec will output the following model module.

# NOTE: This file is auto generated by OpenAPI Generator 7.0.0 (https://openapi-generator.tech).
# Do not edit this file manually.

defmodule Ory.Model.AuthenticatorAssuranceLevel do
  @moduledoc """
  The authenticator assurance level can be one of \"aal1\", \"aal2\", or \"aal3\". A higher number means that it is harder for an attacker to compromise the account.  Generally, \"aal1\" implies that one authentication factor was used while AAL2 implies that two factors (e.g. password + TOTP) have been used.  To learn more about these levels please head over to: https://www.ory.sh/kratos/docs/concepts/credentials
  """

  @derive Jason.Encoder
  defstruct [

  ]

  @type t :: %__MODULE__{

  }

  def decode(value) do
    value
  end
end
Steps to reproduce

Generate any OpenAPI schema with an enum string model and you will get this. For further testing using the Ory SDK, you can checkout my fork and generate the elixir client by commenting out lines 235-333 in scripts/generate.sh and running $ FORCE_PROJECT=client FORCE_VERSION=(cat spec/client/latest) ./scripts/generate.sh to invoke the openapi-generator-cli to generate the client at clients/client/elixir/.

Related issues/PRs

16061 Introduced this regression

Suggest a fix.

Implement a way to deserialize enum modules. Ideally this would go further and implement a word list to validate whether a string value is a valid enum when trying to deserialize into the

tobbbles commented 1 year ago

@barttenbrinke As the Jason->Poison author and the creator of the type decoding magic, any ideas?

tobbbles commented 10 months ago

This is still broken on the new implementation and blocking usage of openapi-generator in Elixir for any string enum components.

barttenbrinke commented 10 months ago

@tobbbles Sorry, this only just appeared in my inbox for some reason /cc @wpiekutowski

barttenbrinke commented 10 months ago

TLDR - Swagger and thus the openapi generator sees the Enum as a authenticatorAssuranceLevel struct, while it basically is just a string. As it is not really doable to handle enums in a struct like this in Elixir, just returning the string might just be the simplest solution here.

We need a simpler example than the whole ORY library

tobbbles commented 10 months ago

Hi @barttenbrinke, thanks for looking around to this. I've made a couple of attempts to fix or investigate this but I don't get any further. What I find very weird is I'm only seeing this behaviour when the string enum is a component/outer enum

This is currently existing in the petstore sample and the generated Elixir client in the repository.

The pet store 3_0 enum test: https://github.com/OpenAPITools/openapi-generator/blob/8e9a17fe0252c1290f585d1fb680c4e64cb5495a/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml#L1653-L1689

generates the following module: https://github.com/OpenAPITools/openapi-generator/blob/master/samples/client/petstore/elixir/lib/openapi_petstore/model/enum_test.ex

Which here we can see there is no issue with enum_string being just a type of String.t; however the outer enum is generated into a module as seen with the Ory example https://github.com/OpenAPITools/openapi-generator/blob/master/samples/client/petstore/elixir/lib/openapi_petstore/model/outer_enum.ex

As it is not really doable to handle enums in a struct like this in Elixir

Would we instead aim to infer the type of out enums and use those as the base type for these fields, compared to generating modules for outer enums? On one hand I like that a module could be generated with a word list for valid enum values; however like you say Elixir doesn't lend itself well to this style of enum.

tobbbles commented 10 months ago

I believe adding the following test case will highlight this:

defmodule EnumTest do
  use ExUnit.Case, async: true
  alias OpenapiPetstore.Deserializer
  alias OpenapiPetstore.Model.EnumTest

  @valid_json """
  {
    "enum_string": "UPPER",
    "outerEnum": "placed"

  }
  """

  test "jason_decode/2 with valid JSON" do
    assert Deserializer.jason_decode(@valid_json, EnumTest) ==
             {:ok,
              %EnumTest{
                enum_string: "UPPER",
                outerEnum: "placed"
              }}
  end
end

With the output:

$ mix test
...

  1) test jason_decode/2 with valid JSON (EnumTest)
     test/enum_test.exs:14
     ** (FunctionClauseError) no function clause matching in OpenapiPetstore.Deserializer.to_struct/2

     The following arguments were given to OpenapiPetstore.Deserializer.to_struct/2:

         # 1
         "placed"

         # 2
         OpenapiPetstore.Model.OuterEnum

     Attempted function clauses (showing 3 out of 3):

         defp to_struct(nil, _)
         defp to_struct(list, module) when is_list(list) and is_atom(module)
         defp to_struct(map, module) when is_map(map) and is_atom(module)

     code: assert Deserializer.jason_decode(@valid_json, EnumTest) ==
     stacktrace:
       (openapi_petstore 1.0.0) lib/openapi_petstore/deserializer.ex:97: OpenapiPetstore.Deserializer.to_struct/2
       (elixir 1.15.7) lib/map.ex:916: Map.update!/3
       (openapi_petstore 1.0.0) lib/openapi_petstore/model/enum_test.ex:36: OpenapiPetstore.Model.EnumTest.decode/1
       (openapi_petstore 1.0.0) lib/openapi_petstore/deserializer.ex:19: OpenapiPetstore.Deserializer.jason_decode/2
       test/enum_test.exs:15: (test)
binajmen commented 4 months ago

@barttenbrinke @tobbbles, apologies for the ping. Is this bug being tracked and worked on? I'm having issues verifying sessions in my Elixir project, which renders the Ory client unusable.

 Frontend.to_session(Ory.Connection.new(), Cookie: cookies(conn))

 [error] ** (FunctionClauseError) no function clause matching in Ory.Deserializer.to_struct/2
barttenbrinke commented 4 months ago

@binajmen The core issue is that there is no simple test / example that reproduces the issue at the moment. All enum tests work in the OpenAPITools suite and the simple test @tobbbles made also succeeds. I am not working on it atm, if somebody creates a test / example that reproduces the error I would be able to fix it quite quickly I think.

binajmen commented 4 months ago

I would love to help, but I'm clueless about where to start. @tobbbles, is there a reason why your test succeeded with the OpenAPITools suite?

tobbbles commented 3 months ago

Hi folks, finally got some vacation time to circle back on this. Are we sure the example I posted succeeds? With a fresh checkout I have the following; however in https://github.com/OpenAPITools/openapi-generator/pull/19435 where I introduce this test case, it looks like the CI is not executing it.

Could someone check to reproduce as I have please?

$ cat >samples/client/petstore/elixir/test/issue.exs<<EOF

defmodule IssueTest do
  use ExUnit.Case, async: true

  alias OpenapiPetstore.Deserializer
  alias OpenapiPetstore.Model.EnumTest

  @valid_json """
  {
    "enum_string": "UPPER",
    "outerEnum": "placed"
  }
  """

  test "jason_decode/2 with valid JSON" do
    assert Deserializer.jason_decode(@valid_json, EnumTest) ==
             {:ok,
              %EnumTest{
                enum_string: "UPPER",
                outerEnum: "placed"
              }}
  end
end

EOF 

$ cd samples/client/petstore/elixir

$ mix deps.get
...

$ mix test test/issue.exs
Running ExUnit with seed: 24428, max_cases: 64

  1) test jason_decode/2 with valid JSON (IssueTest)
     test/issue.exs:14
     ** (FunctionClauseError) no function clause matching in OpenapiPetstore.Deserializer.to_struct/2

     The following arguments were given to OpenapiPetstore.Deserializer.to_struct/2:

         # 1
         "placed"

         # 2
         OpenapiPetstore.Model.OuterEnum

     Attempted function clauses (showing 3 out of 3):

         defp to_struct(nil, _)
         defp to_struct(list, module) when is_list(list) and is_atom(module)
         defp to_struct(map, module) when is_map(map) and is_atom(module)

     code: assert Deserializer.jason_decode(@valid_json, EnumTest) ==
     stacktrace:
       (openapi_petstore 1.0.0) lib/openapi_petstore/deserializer.ex:97: OpenapiPetstore.Deserializer.to_struct/2
       (elixir 1.17.2) lib/map.ex:916: Map.update!/3
       (openapi_petstore 1.0.0) lib/openapi_petstore/model/enum_test.ex:36: OpenapiPetstore.Model.EnumTest.decode/1
       (openapi_petstore 1.0.0) lib/openapi_petstore/deserializer.ex:19: OpenapiPetstore.Deserializer.jason_decode/2
       test/issue.exs:15: (test)

Finished in 0.05 seconds (0.05s async, 0.00s sync)
1 test, 1 failure
tobbbles commented 3 months ago

I think the main root issue here is the generated enum modules are just empty struct,

iex(3)> OpenapiPetstore.Model.OuterEnum.__struct__
%OpenapiPetstore.Model.OuterEnum{}

Which is more or less unusable.