Giter VIP home page Giter VIP logo

open_api_spex's People

Contributors

aisrael avatar albertored avatar brentjr avatar bryannaegele avatar feng19 avatar fenollp avatar ggpasqualino avatar holsee avatar lucacorti avatar mbuhot avatar mojidabckuu avatar moxley avatar mrmstn avatar msutkowski avatar nurugger07 avatar oliver-schoenherr avatar palcalde avatar shikanime avatar slapers avatar slavo2 avatar supermaciz avatar surik avatar tapickell avatar tehprofessor avatar vovayartsev avatar wingyplus avatar xadhoom avatar zakjholt avatar zorbash avatar zoten avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

open_api_spex's Issues

Fails to generate

I can view the Swagger just fine, but when I come to generate it, I get

** (ArgumentError) argument error
    (stdlib) :ets.lookup(EmbedWeb.Endpoint, :__phoenix_struct_url__)
    lib/phoenix/config.ex:45: Phoenix.Config.cache/3
    (open_api_spex) lib/open_api_spex/server.ex:39: OpenApiSpex.Server.from_endpoint/1
    lib/embed/api_spec.ex:9: Embed.V1.ApiSpec.spec/0
    lib/embed/api_task.ex:6: Mix.Tasks.Embed.OpenApiSpec.run/1
    (mix) lib/mix/task.ex:331: Mix.Task.run_task/3
    (mix) lib/mix/cli.ex:79: Mix.CLI.run_task/2

I'm not sure what's missing. The Spec and Task modules are created correctly.

Allow defining operations by module attributes

Example implementation that I have used in my project (still lacks a little bit options, but overall idea can be seen):

defmodule KokonWeb.Rest do
  alias OpenApiSpex.Operation

  defmacro __using__(_opts) do
    quote do
      alias KokonWeb.Rest.Schema
      alias OpenApiSpex.Operation

      plug(OpenApiSpex.Plug.Cast)
      plug(OpenApiSpex.Plug.Validate)

      @on_definition KokonWeb.Rest
      @before_compile KokonWeb.Rest

      Module.register_attribute(__MODULE__, :parameter, accumulate: true)
      Module.register_attribute(__MODULE__, :response, accumulate: true)

      Module.register_attribute(__MODULE__, :open_api_operations, accumulate: true)
    end
  end

  def __on_definition__(_env, _type, :open_api_operation, _args, _guards, _body), do: nil
  def __on_definition__(_env, _type, :action, _args, _guards, _body), do: nil

  def __on_definition__(%Macro.Env{module: mod}, :def, name, _args, _guards, _body) do
    parameters = Module.delete_attribute(mod, :parameter)
    response = Module.delete_attribute(mod, :response)
    {summary, doc} = docs(Module.get_attribute(mod, :doc))

    operation =
      %Operation{
        summary: summary || "TODO",
        description: doc,
        operationId: module_name(mod) <> ".#{name}",
        parameters: parameters,
        responses: Map.new(response)
      }

    Module.put_attribute(mod, :open_api_operations, {name, operation})
  end

  def __on_definition__(_env, _type, _name, _args, _guards, _body), do: nil

  defmacro __before_compile__(_env) do
    quote unquote: false do
      for {name, operation} <- Module.delete_attribute(__MODULE__, :open_api_operations) do
        def open_api_operation(unquote(name)), do: unquote(Macro.escape(operation))
      end
    end
  end

  defp docs(nil), do: {nil, nil}

  defp docs({_, doc}) do
    [summary | _] = String.split(doc, ~r/\n\s*\n/, parts: 2)

    {summary, doc}
  end

  defp module_name(mod) when is_atom(mod), do: module_name(Atom.to_string(mod))
  defp module_name("Elixir." <> name), do: name
  defp module_name(name) when is_binary(name), do: name
end

This causes controller to look like this:

defmodule KokonWeb.Rest.Controllers.Schedule do
  use KokonWeb.Rest

  @doc "List schedule"
  @response {200, Operation.response(
    "Submissions",
    "application/json",
    KokonWeb.Rest.Schema.Submissions
  )}
  def index(conn, _params) do
    {:ok, submissions} = Kokon.Submissions.all()

    json(conn, submissions)
  end

  @doc "Create new submission"
  @parameter Operation.parameter(:title, :query, :string, "Submission title",
    required: true
  )
  @parameter Operation.parameter(
    :abstract,
    :query,
    :string,
    "Submission description",
    required: true
  )
  @response {200, Operation.response(
    "Submissions",
    "application/json",
    KokonWeb.Rest.Schema.Submission
  )}
  def create(conn, %{title: title, abstract: abstract}) do
    with {:ok, submission} <-
      Kokon.Submissions.create(%{title: title, abstract: abstract}) do
      json(conn, submission)
    end
  end
end

I am opening this as an issue instead of PR as I would like to know opinions about this beforehand.

Casted conn.params and conn.body_params do not match type spec. Dialyzer raises warnings.

The type spec for params and body_params is params() :: %{required(binary()) => param()} (https://hexdocs.pm/plug/1.7.2/Plug.Conn.html#t:params/0), but OpenApiSpex turns them into something like %{__struct__: module(), required(atom()) => term()}, or something else that doesn't match the spec. This causes dialyzer complaints in the controller actions for some situations.

IMO, OpenApiSpex shouldn't change conn.params or conn.body_params, as it breaks the contract with Plug. Instead, Spex can put the casted values in the :private attribute of the %Conn{}.

Dialyzer warnings on controller actions caused by CastAndValidate use. Casted request body not conforming to `Plug.Conn.t()` type

When adding casted value to the connection body params this raises dialyzer errors as this does not meet the specification of Plug.Conn.t() where only atom and binary are allowed specificatallly.

  %Plug.Conn{
    :adapter => {atom(), _},
    :assigns => %{atom() => _},
    :before_send => [(map() -> map())],
    :body_params => %Plug.Conn.Unfetched{
      :aspect => atom(),
      binary() =>
        binary() | [binary() | [any()] | map()] | %{binary() => binary() | [any()] | map()}
    },
...

Recommendation

In order to meet the specification of Plug.Conn.t() the casted values will need to be put into the body params like so:

conn = %{body_params: %{"request" => %SomeRequest{}}}

Or I will need to make the request schema an unstructured map to enable this without a code change which feels bad as it adds a layer of nesting. I will try this out.

This might be a non-issue, but at the very least this will need documented as request_body function pushes users in this direction during schema definition.

          request_body(
            "Integration registration attributes",
            "application/json",
            __MODULE__.RegistrationRequest
          ),

It is unfortunate that this will require: @dialyzer {:nowarn_function, {:register, 2}} or a less strict exclusion such as :no_return (see examples) as one of the benefits of the casting is the ability to perform this type checking.

Examples:

Controller Action

  def register(conn = %{body_params: request}, _params) do
    %RegistrationRequest{} = request
    render(conn, "not_implemented.json")
  end

The inference of the body_params type as a Struct (as above) is enough for dialyzer to warn:

apps/bond_web/lib/bond_web/controllers/integration_controller.ex:9:no_return
Function register/2 has no local return.

This report becomes more specific when you put the destructure into the function param like so:

  def register(conn = %{body_params: %RegistrationRequest{} = request}, _params) do
    render(conn, "not_implemented.json")
  end
Phoenix.Controller.render(
  _conn :: %{
    :body_params => %BondWeb.Schemas.Integrations.Operations.RegistrationRequest{_ => _},
    _ => _
  },
  <<_::160>>
)

will never return since the success typing is:
(
  %Plug.Conn{
    :adapter => {atom(), _},
    :assigns => %{atom() => _},
    :before_send => [(map() -> map())],
    :body_params => %Plug.Conn.Unfetched{
      :aspect => atom(),
      binary() =>
        binary() | [binary() | [any()] | map()] | %{binary() => binary() | [any()] | map()}
    },
    :cookies => %Plug.Conn.Unfetched{:aspect => atom(), binary() => binary()},
    :halted => _,
    :host => binary(),
    :method => binary(),
    :owner => pid(),
    :params => %Plug.Conn.Unfetched{
      :aspect => atom(),
      binary() =>
        binary() | [binary() | [any()] | map()] | %{binary() => binary() | [any()] | map()}
    },
    :path_info => [binary()],
    :path_params => %{
      binary() =>
        binary() | [binary() | [any()] | map()] | %{binary() => binary() | [any()] | map()}
    },
    :port => char(),
    :private => %{atom() => _},
    :query_params => %Plug.Conn.Unfetched{
      :aspect => atom(),
      binary() =>
        binary() | [binary() | [any()] | map()] | %{binary() => binary() | [any()] | map()}
    },
    :query_string => binary(),
    :remote_ip =>
      {byte(), byte(), byte(), byte()}
      | {char(), char(), char(), char(), char(), char(), char(), char()},
    :req_cookies => %Plug.Conn.Unfetched{:aspect => atom(), binary() => binary()},
    :req_headers => [{binary(), binary()}],
    :request_path => binary(),
    :resp_body =>
      nil
      | binary()
      | maybe_improper_list(
          binary() | maybe_improper_list(any(), binary() | []) | byte(),
          binary() | []
        ),
    :resp_cookies => %{binary() => %{}},
    :resp_headers => [{binary(), binary()}],
    :scheme => :http | :https,
    :script_name => [binary()],
    :secret_key_base => nil | binary(),
    :state => :chunked | :file | :sent | :set | :set_chunked | :set_file | :unset,
    :status => nil | non_neg_integer()
  },
  atom() | binary() | Keyword.t() | map()
) :: %Plug.Conn{
  :adapter => {atom(), _},
  :assigns => %{atom() => _},
  :before_send => [(map() -> map())],
  :body_params => %Plug.Conn.Unfetched{
    :aspect => atom(),
    binary() =>
      binary() | [binary() | [any()] | map()] | %{binary() => binary() | [any()] | map()}
  },
  :cookies => %Plug.Conn.Unfetched{:aspect => atom(), binary() => binary()},
  :halted => _,
  :host => binary(),
  :method => binary(),
  :owner => pid(),
  :params => %Plug.Conn.Unfetched{
    :aspect => atom(),
    binary() =>
      binary() | [binary() | [any()] | map()] | %{binary() => binary() | [any()] | map()}
  },
  :path_info => [binary()],
  :path_params => %{
    binary() =>
      binary() | [binary() | [any()] | map()] | %{binary() => binary() | [any()] | map()}
  },
  :port => char(),
  :private => %{atom() => _},
  :query_params => %Plug.Conn.Unfetched{
    :aspect => atom(),
    binary() =>
      binary() | [binary() | [any()] | map()] | %{binary() => binary() | [any()] | map()}
  },
  :query_string => binary(),
  :remote_ip =>
    {byte(), byte(), byte(), byte()}
    | {char(), char(), char(), char(), char(), char(), char(), char()},
  :req_cookies => %Plug.Conn.Unfetched{:aspect => atom(), binary() => binary()},
  :req_headers => [{binary(), binary()}],
  :request_path => binary(),
  :resp_body =>
    nil
    | binary()
    | maybe_improper_list(
        binary() | maybe_improper_list(any(), binary() | []) | byte(),
        binary() | []
      ),
  :resp_cookies => %{binary() => %{}},
  :resp_headers => [{binary(), binary()}],
  :scheme => :http | :https,
  :script_name => [binary()],
  :secret_key_base => nil | binary(),
  :state => :sent,
  :status => nil | non_neg_integer()
}

and the contract is
(Plug.Conn.t(), Keyword.t() | map() | binary() | atom()) :: Plug.Conn.t()

Optionally exclude some fields when serializing

  • Values that are empty such as tags security
  • Values from schemas such as x-struct, title

I believe that since serialization is not part of the lib this has to be done somewhere else, but maybe there's a Protocol to implement that can use the "omitempty" idiom of Golang? I may be wrong as I am somewhat new to Elixir.

Thanks

resolve_schema_modules/1 do not resolve nested schemas

When I define result schema as:

Operation.response(
  "Submissions",
  "application/json",
  %OpenApiSpex.Schema{
    title: "Submissions",
    description: "List of submissions",
    type: :array,
    items: KokonWeb.Rest.Schema.Submission
  }
)

Then in the the output it is defined as:

{
  "description": "Submissions",
  "content": {
    "application/json": {
      "schema": {
        "type": "array",
        "title": "Submissions",
        "items": "Elixir.KokonWeb.Rest.Schema.Submission",
        "description": "List of submissions"
      }
    }
  }
}

Where I would expect items to be reference to Submission definition.

Call for maintainers!

I'm no longer using swagger in my day job, so I haven't had the need to make many improvements to this package.

If there are any users who are relying on open_api_spex in commercial / production settings that are willing to share in the maintenance of the project, please respond in the comments, or email me ([email protected]).

I'm happy to move this repo to a Github org and share ownership of the Hex package.

cc: @ThomasRueckert @fenollp @slavo2

Validate schemas using ex_json_schema

Following on #23 (and my attempt at solving it: #42), I have been instead using the really good ex_json_schema.

Here is the code I am using:

    case %{
           "components" => components,
           "additionalProperties" => false,
           "type" => "object",
           "required" => [schema_name],
           "properties" => %{
             schema_name => %{"$ref" => "#/components/schemas/#{schema_name}"}
           }
         }
         |> ExJsonSchema.Schema.resolve()
         |> ExJsonSchema.Validator.validate(resp)

There needs to be a translation step first though, to rewrite nullable: true and such incompatibilities of OpenAPIV3 schemas with JSON Schema Draft-04.

Would you be open to use ex_json_schema validators instead of the current ones in this lib?
Where do you think this translation step should be: within this lib or within ex_json_schema?

Thanks

schema: load from file and validate request and response

Hi,

Is this possible, given I have a request and a response that I wish to validate. I'd like to:

  1. Load schema from file.
  2. Call a validate function in the API for the request.
  3. Call a validate function in the API for the response.

I don't want to use Plugs, I'd like to use it as a pure API.

How to write callbacks?

I see there is support for defining out of band callbacks which are part of an operation.
https://swagger.io/specification/#callbackObject

I cannot figure out how this should be written using OpenAPISpex as I am running into some strange errors. Can you please provide an example of how callbacks should be written?

I see Operation has a key callbacks, this takes a map of String.t =>PathItem.t.
The PathItem.t has fields which each take an Operation.t.

      callbacks: %{
        "account_link_callback" => %PathItem{
          post: %Operation{
            summary: "Account Link Callback",
            description:
              "Invoked when account link is established or if the account link process fails",
            responses:
              response(
                "List registered integrations response",
                "application/json",
                __MODULE__.ListResponse
              )
          }
        }

This produces:

image

Which from the outset looks OK?
But there is likely an issue as it renders like so:

image

Exposing Swagger endpoint raises Dialyzer issues

If I expose the swagger endpoint in my Phoenix router with:

    scope "/" do
      _ = get("/swagger", OpenApiSpex.Plug.SwaggerUI, path: "/api/openapi")
    end

I get the error

    unmatched_return
    Expression produces a value of type:

    %{:path => _}

    but this value is unmatched.

I'm assuming the issue isn't actually in the router, but the module itself. Is that right?

:type property of Schema struct should not be required

I need to represent a parameter that can be an integer or a string in this way:

%OpenApiSpex.Schema{
  oneOf: [
    %OpenApiSpex.Schema{type: :integer},
    %OpenApiSpex.Schema{type: :string}
  ]
}

but this results in error

** (ArgumentError) the following keys must also be given when building struct OpenApiSpex.Schema: [:type]

The same happens using references and it the documentation example

%OpenApiSpex.Schema{
  oneOf: [
    %OpenApiSpex.Reference{"$ref": "#/components/schemas/Foo"},
    %OpenApiSpex.Reference{"$ref": "#/components/schemas/Bar"}
  ]
}

When Content Type Header Missing`** (UndefinedFunctionError) function nil.schema/0 is undefined`

I am seeing the following error when testing a controller action (with the stock Phoenix ConnCase style tests):

  1) test POST /api/v0/integrations/register produces a RegistrationSuccessfulResponse (BondWeb.IntegrationControllerTest)
     test/bond_web/controllers/integration_controller_test.exs:26
     ** (UndefinedFunctionError) function nil.schema/0 is undefined
     code: |> post(integration_path(conn, :register), registration_args)
     stacktrace:
       nil.schema()
       (open_api_spex) lib/open_api_spex/operation2.ex:35: OpenApiSpex.Operation2.cast_request_body/4
       (open_api_spex) lib/open_api_spex/operation2.ex:21: OpenApiSpex.Operation2.cast/4
       (open_api_spex) lib/open_api_spex/plug/cast_and_validate.ex:70: OpenApiSpex.Plug.CastAndValidate.call/2
       (bond_web) lib/bond_web/controllers/integration_controller.ex:1: BondWeb.IntegrationController.phoenix_controller_pipeline/2
       (bond_web) lib/bond_web/endpoint.ex:1: BondWeb.Endpoint.instrument/4
       (phoenix) lib/phoenix/router.ex:275: Phoenix.Router.__call__/1
       (bond_web) lib/bond_web/endpoint.ex:1: BondWeb.Endpoint.plug_builder_call/2
       (bond_web) lib/bond_web/endpoint.ex:1: BondWeb.Endpoint.call/2
       (phoenix) lib/phoenix/test/conn_test.ex:235: Phoenix.ConnTest.dispatch/5
       test/bond_web/controllers/integration_controller_test.exs:39: (test)

I can see that that the error is happening within the plug OpenApiSpex.Plug.CastAndValidate however when I call the API when the Phoenix server is running, this plug does not fail and the Casting and Validation works fine.

This leads me to conclude that something is not happening (possibly some information in conn?) in test that is present during development execution.

This may not be a bug, but it does seem that there is an initialisation step needed in testing to avoid this issue?

Appreciate any help in this matter, love the project <3!

Bump version

Hi @mbuhot,
could you pls release the latest patches to a new hex version?
Or are there any metrics on which you decide to do so?

Cheers.

Duplicated operationId for multiple routes with same controller function

I'm using a lot of Phoenix Resource Controllers in my project.

Since the 'update' function is registered on the patch and put routes for the resource controller, there will also be a patch and put call in the openapi spec.

The problem now is, that the definition for the update function is identicaly on both calls, including the operation id.

This isn't actually just e resource controller problem. Duplicated operation Ids will be present as soon as multiple routes lead to the same controller function.

Better validation error format

When we get a validation error in the fallback controller we receive a string in the body for the error.
This string is hard to parse and we have started down the path of trying to parse these strings to get the path of the invalid property in order to produce proper json api error responses that are meaningful to our API consumers.
I would like to propose returning something easier to work with within the code.
Perhaps returning a Map with some keys we can get the info from without parsing strings.

Examples:
(Current Error)
"#/data: Missing required properties: [:relationships]"
could be something like

%{
  path: ["data", "relationships"],
  message: "Missing required properties"
}

and
(Current Error)
"#/data/attributes/resource: null value where string expected"
could be something like

%{
  path: ["data", "attributes", "resource"],
  message: "null value where string expected"
}

We should have a way to represent paths that have arrays in them as well
"#/data/0/id: null value where string expected"

%{
  path: ["data", 0, "id"],
  message: "null value where string expected"
}

We would be happy to create a PR for this, we wanted to propose the idea and get some feedback before going forward with this approach.
We also may be interested in maintaining this project as well as it looks like we will be using it extensively for our applications.
https://github.com/GhostGroup
Thank you

Allow customising JSON encoders

Currently the community is mostly migrating to jason library due to it's performance benefits. It would be worth making JSON encoder configurable so when in need someone could change it to fit their own preferences and/or needs (for example to use streaming parser instead).

Support assert_schema for array type

If I have a schema like the following

    OpenApiSpex.schema(%{
      title: "Attributes",
      type: :array,
      items: Api.Schemas.Attribute
    })
  end

would be nice to do assert_schema("Attributes", spec)

This at the moment does not work as assert_schema accepts a map.
Is it something something that can be added?

OpenApiSpex.Cast.String.cast/1 not casting dates or date-times

cast/1 should cast string types with a format of :date to a %Date{}, and a format of :"date-time" to %DateTime{}. Instead, it passes the input string untouched.

The old cast function casted date and date-time strings to their respective Elixir types, but the new module doesn't.

Here are the invalid tests:

Some issues when casting and validating oneOf polymorphic schemas

We are using open_api_spex to document our apis and slowly adding schema validation in our tests. However we are seeing some unexpected result and errors mostly to using the oneOf schemas.

In order to describe what we are seeing (maybe its because we are not correctly setting up the schema) we created a small scenario using the example on the swagger.io site: https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/#oneof

On the swagger site 3 examples are given with explanation why data should be valid or invalid. We implemented these as test, unfortunately we cannot seem to make the tests pass.

defmodule OAS do
  require OpenApiSpex
  alias OpenApiSpex.{Schema, OpenApi, Components}

  defmodule Cat,
    do:
      OpenApiSpex.schema(%{
        title: "Cat",
        type: :object,
        properties: %{
          hunts: %Schema{type: :boolean},
          age: %Schema{type: :integer}
        }
      })

  defmodule Dog,
    do:
      OpenApiSpex.schema(%{
        title: "Dog",
        type: :object,
        properties: %{
          bark: %Schema{type: :boolean},
          breed: %Schema{type: :string, enum: ["Dingo", "Husky", "Retriever", "Shepherd"]}
        }
      })

  defmodule CatOrDog,
    do:
      OpenApiSpex.schema(%{
        title: "CatOrDog",
        anyOf: [OAS.Cat, OAS.Dog]
      })

  def spec() do
    schemas =
      for module <- [OAS.Cat, OAS.Dog, OAS.CatOrDog], into: %{} do
        {module.schema().title, module.schema()}
      end

    %OpenApi{info: %{}, paths: %{}, components: %Components{schemas: schemas}}
    |> OpenApiSpex.resolve_schema_modules()
  end
end

defmodule OASTest do
  @moduledoc false

  use ExUnit.Case
  alias OpenApiSpex.Schema

  @api_spec OAS.spec()
  @schema @api_spec.components.schemas["CatOrDog"]

  test "casting yields strange data" do
    input = %{"bark" => true, "breed" => "Dingo"}

    assert {:ok, %OAS.Dog{bark: true, breed: "Dingo"}} =
             OpenApiSpex.cast(@api_spec, @schema, input)
  end

  test "should be invalid (not valid against both schemas)" do
    input = %{"bark" => true, "hunts" => true}
    refute :ok == OpenApiSpex.validate(@api_spec, @schema, input)
  end

  test "should be invalid (valid against both)" do
    input = %{"bark" => true, "hunts" => true, "breed" => "Husky", "age" => 3}
    refute :ok == OpenApiSpex.validate(@api_spec, @schema, input)
  end
end

Are we missing something ? Any suggestion would be awesome !

Validate without casting to struct

There is no way to validate requests without also casting the request to a struct, as opposed to a simple map. We haven't found any benefit of receiving structs-- only drawbacks.

The two drawbacks we have found are:

  1. In certain situations, we have to call Map.from_struct/1 on the casted struct, because the __struct__ key interferes with what we're trying to do.
  2. When an object has optional properties, they are always present in the output of the cast function, even when they're absent in the input. This is because the output is a struct, and structs always have all the keys in their definitions. Sometimes, this causes undesirable results, and extra care has to be take to work around these always-present keys.

Would anyone please list specific benefits of returning structs for the cast operation?

Issue using :oneOf

Hi,

Im currently figuring out, if we should use open_api_spex in our project. There we have the need to use :oneOf (https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/#oneof).

I could not figure out, how to use it. My schema is similar to the following example:

defmodule MySchema do
  alias OpenApiSpex.Schema

  @behaviour OpenApiSpex.Schema
  @derive [Poison.Encoder]
  @schema %Schema{
    title: "MySchema",
    type: :object,
    oneOf: [
      MyOtherSchemaA,
      MyOtherSchemaB
    ],
    properties: %{},
    "x-struct": __MODULE__
  }
  def schema, do: @schema
  defstruct Map.keys(@schema.properties)
end

Running this, give me the following stacks track:

2018-05-25 14:17:53.319  [error] #PID<0.1238.0> running Web.Endpoint terminated
Server: localhost:4000 (http)
Request: GET /api/v1/doc
** (exit) an exception was raised:
    ** (FunctionClauseError) no function clause matching in OpenApiSpex.SchemaResolver.resolve_schema_modules_from_schema/2
        (open_api_spex) lib/open_api_spex/schema_resolver.ex:133: OpenApiSpex.SchemaResolver.resolve_schema_modules_from_schema([MyOtherSchemaA, MyOtherSchemaB], %{})
        (open_api_spex) lib/open_api_spex/schema_resolver.ex:149: OpenApiSpex.SchemaResolver.resolve_schema_modules_from_schema/2
        (open_api_spex) lib/open_api_spex/schema_resolver.ex:172: anonymous fn/2 in OpenApiSpex.SchemaResolver.resolve_schema_modules_from_schema_properties/2
        (stdlib) lists.erl:1263: :lists.foldl/3
        (open_api_spex) lib/open_api_spex/schema_resolver.ex:154: OpenApiSpex.SchemaResolver.resolve_schema_modules_from_schema/2
        (open_api_spex) lib/open_api_spex/schema_resolver.ex:142: OpenApiSpex.SchemaResolver.resolve_schema_modules_from_schema/2
        (open_api_spex) lib/open_api_spex/schema_resolver.ex:98: OpenApiSpex.SchemaResolver.resolve_schema_modules_from_media_type/2
        (open_api_spex) lib/open_api_spex/schema_resolver.ex:92: anonymous fn/2 in OpenApiSpex.SchemaResolver.resolve_schema_modules_from_content/2
        (stdlib) lists.erl:1263: :lists.foldl/3
        (open_api_spex) lib/open_api_spex/schema_resolver.ex:121: OpenApiSpex.SchemaResolver.resolve_schema_modules_from_response/2
        (open_api_spex) lib/open_api_spex/schema_resolver.ex:115: anonymous fn/2 in OpenApiSpex.SchemaResolver.resolve_schema_modules_from_responses/2
        (stdlib) lists.erl:1263: :lists.foldl/3
        (open_api_spex) lib/open_api_spex/schema_resolver.ex:61: OpenApiSpex.SchemaResolver.resolve_schema_modules_from_operation/2
        (open_api_spex) lib/open_api_spex/schema_resolver.ex:53: anonymous fn/2 in OpenApiSpex.SchemaResolver.resolve_schema_modules_from_path_item/2
        (elixir) lib/enum.ex:1899: Enum."-reduce/3-lists^foldl/2-0-"/3
        (open_api_spex) lib/open_api_spex/schema_resolver.ex:43: anonymous fn/2 in OpenApiSpex.SchemaResolver.resolve_schema_modules_from_paths/2
        (stdlib) lists.erl:1263: :lists.foldl/3
        (open_api_spex) lib/open_api_spex/schema_resolver.ex:36: OpenApiSpex.SchemaResolver.resolve_schema_modules/1
        (open_api_spex) lib/open_api_spex/plug/put_api_spec.ex:36: OpenApiSpex.Plug.PutApiSpec.build_spec/1
        (open_api_spex) lib/open_api_spex/plug/put_api_spec.ex:20: OpenApiSpex.Plug.PutApiSpec.call/2

How do I define a schema which should consists of one of two other schemas?

Any help is appreciated.

Cheers,
Tobias

Not able to generate spec.json by following the docs

I was trying to generate the json docs for a module I am working on and wasn't able to. After some time trying I decided to try with a clean phoenix server project. Copying and pasting the code from the docs and running the command:

mix apitest.open_api_spec spec.json

I couldn't get the command to work, getting the same error I have in my own personal project. Which is this:

** (ArgumentError) argument error
    (stdlib) :ets.lookup(ApitestWeb.Endpoint, :__phoenix_struct_url__)
    (phoenix) lib/phoenix/config.ex:42: Phoenix.Config.cache/3
    lib/open_api_spex/server.ex:39: OpenApiSpex.Server.from_endpoint/1
    lib/api_spec.ex:11: ApitestWeb.ApiSpec.spec/0
    lib/apitest/openapispec.ex:4: Mix.Tasks.Apitest.OpenApiSpec.run/1
    (mix) lib/mix/task.ex:331: Mix.Task.run_task/3
    (mix) lib/mix/cli.ex:79: Mix.CLI.run_task/2
    (elixir) lib/code.ex:767: Code.require_file/2

Not sure exactly what is going on but I am following the getting started docs to the letter. I can share the code if it helps.

And I am using phoenix v1.4.6

I didn't want to put a lot of information as to not bloat the issue, but I'll be more than happy to provide any information you deem relevant.

Cut a release

@mbuhot could we get a release cut and pushed to hex? There's been a lot of updates and fixes since the last release at the end of October.

Thanks!

Validation of DeepObject properties in query

Hi Mike

Lets say one have a following query

/books?filter[name]=Encyclopedia&filter[edition]=2&filter[published]=2018-01-01

i.e. there is a "deepObject" (https://swagger.io/docs/specification/serialization/) in the query,
called filter, which can (but does not have to) contain properties with specific format (string, number, date).

How one could make an operation spec, using open_api_spex,
so that OpenApiSpex.Plug.Validate would properly validate its parts?

Thanks

Slavo

Can not cast GET request without content-type header

After merging #45 it is not possible anymore to cast GET request, because of GET request does not contains request body, no content type header is required, which leads to this exception:

  ** (FunctionClauseError) no function clause matching in String.split/3

     The following arguments were given to String.split/3:
     
         # 1
         nil
     
         # 2
         ";"
     
         # 3
         []
     
     Attempted function clauses (showing 4 out of 4):
     
         def split(string, %Regex{} = pattern, options) when is_binary(string)
         def split(string, "", options) when is_binary(string)
         def split(string, pattern, []) when is_tuple(pattern) or is_binary(string)
         def split(string, pattern, options) when is_binary(string)
     
     code: conn = .....
     stacktrace:
       (elixir) lib/string.ex:383: String.split/3
       (open_api_spex) lib/open_api_spex/plug/cast.ex:58: OpenApiSpex.Plug.Cast.call/2

Need more robust "type" arg validation in OpenApiSpex.schema macro

It's too easy to make silly mistakes in OpenApiSpex.schema macro :-)
My mistake was omitting type field of a regular (non-polymorphic) schema.

    OpenApiSpex.schema(
      %{
        title: "Response",
        # type: :object" <-- it's missing here
        properties: %{
          page: %Schema{type: :integer, required: true},
          hitsPerPage: %Schema{type: :integer},
          ....
        }
      }

As a result, testing the response structure in my test suite didn't really check the properties

      # this always succeeded
      assert_schema(response, "Response", api_spec)

I guess the validation was halting at this code in schema.ex

  def validate(%Schema{type: nil}, _value, _path, _schemas) do
    # polymorphic schemas will terminate here after validating against anyOf/oneOf/allOf/not
    :ok
  end

What do you think about adding some checks to OpenApiSpex.schema macro to avoid at least this particular silly but dangerous mistake?

Possible improvement to obtaining url from Endpoint for Server Spec

@doc """
Builds a Server from a phoenix Endpoint module
"""
@spec from_endpoint(module, [otp_app: atom]) :: t
def from_endpoint(endpoint, opts) do
app = opts[:otp_app]
url_config = Application.get_env(app, endpoint, []) |> Keyword.get(:url, [])
scheme = Keyword.get(url_config, :scheme, "http")
host = Keyword.get(url_config, :host, "localhost")
port = Keyword.get(url_config, :port, "80")
path = Keyword.get(url_config, :path, "/")
%Server{
url: "#{scheme}://#{host}:#{port}#{path}"
}
end

I feel this can be better achieved through:

%Server{
  url: MyApp.Endpoint.url()
}

Fix spec issue introduced in PR #69

A concern about divergence of the Open API spec was brought up here: #69 (comment)

Adjust the behavior in the string validation logic. The current behavior is to trim the string before validating string length. Change it to not trim the string before validating string length. The reason for trimming was to provide a cleaner way to validate non-blankness. However, this is not supported by the spec. The workaround for validating non-blankness is to use a regular expression validation with the pattern /^\S+$/.

Allow disabling cache in OpenApiSpex.Plug.PutApiSpec

Caching can be problematic in development and cleaning configuration after each recompilation can be troublesome.

Additionally such behaviour can be problematic when doing hot upgrades.

One possible solution is to store mod.module_info(:md5) along the cached data and drop everything on hash change.

raise exception on invalid query parameter

Hi @mbuhot, if an undefined query parameter is transmitted the cast plug (and also the validate plug) is just ignoring this parameter. I think it would be better for the api user to respond with an undefined parameter error. Otherwise it is hard for the consumer to debug unexpected behavior.

What do you think?

Cheers.

Casting using oneOf not implemented?

Hi there ๐Ÿ‘‹

This probably relates to #23

First off, thanks a lot for all the work to put this library together!
I've had a great time using it to document some Elixir APIs.

Recently I've been trying to invest some effort in building better schemas and let OpenApiSpex.Plug.Cast cast them into structs. However I ran into a few issues when trying to use polymorphic schemas.

For instance if I have the following schemas:

defmodule OpenApiSpexOneOfDemo.PetPlug.PetRequest do
  require OpenApiSpex

  alias OpenApiSpex.Discriminator
  alias OpenApiSpexOneOfDemo.Schemas.{Cat, Dog}

  OpenApiSpex.schema(%{
    title: "PetRequest",
    type: :object,
    oneOf: [Cat, Dog],
    discriminator: %Discriminator{
      propertyName: "pet_type"
    },
    example: %{"pet_type" => "Cat", "meow" => "meow"}
  })
end

defmodule OpenApiSpexOneOfDemo.Schemas do
  defmodule Cat do
    require OpenApiSpex

    alias OpenApiSpex.Schema

    OpenApiSpex.schema(%{
      title: "Cat",
      type: :object,
      properties: %{
        pet_type: %Schema{type: :string},
        meow: %Schema{type: :string}
      },
      required: [:pet_type, :meow]
    })
  end

  defmodule Dog do
    require OpenApiSpex

    alias OpenApiSpex.Schema

    OpenApiSpex.schema(%{
      title: "Dog",
      type: :object,
      properties: %{
        pet_type: %Schema{type: :string},
        bark: %Schema{type: :string}
      },
      required: [:pet_type, :bark]
    })
  end
end

I would expect posting the following JSON:

{
  "pet_type": "Cat",
  "meow": "meow"
}

To be cast into the following struct when using the OpenApiSpex.Plug.Cast plug:

%OpenApiSpexOneOfDemo.Schemas.Cat{meow: "meow", pet_type: "Cat"}

However, in my tests, it ends up being cast into:

%OpenApiSpexOneOfDemo.PetPlug.PetRequest{}

Having gone through the code a bit, it seems that casting into polymorphic schemas is simply not implemented... ๐Ÿ˜ข

Is that the case or / and I doing something wrong?
If it hasn't been implemented yet, is there anything I can do to help?
Any pointers towards what would need to be done?
Do you have any idea how much effort you think it would take this implemented?
As a first step, only supporting polymorphic schemas that use discriminators might make this easier to implement. What do you think?

The full code for my test from which the above snippets are taken from can be found here: maxmellen/open_api_spex_one_of_demo

Running mix test will showcase the issue I am talking about here:

  1) test {"pet_type": "Cat", "meow": "meow"} is cast into %OpenApiSpexOneOfDemo.Schemas.Cat{} (OpenApiSpexOneOfDemo.PetPlugTest)
     test/open_api_spex_one_of_demo/pet_plug_test.exs:8
     Assertion with == failed
     code:  assert conn.resp_body() == "%OpenApiSpexOneOfDemo.Schemas.Cat{meow: \"meow\", pet_type: \"Cat\"}"
     left:  "%OpenApiSpexOneOfDemo.PetPlug.PetRequest{}"
     right: "%OpenApiSpexOneOfDemo.Schemas.Cat{meow: \"meow\", pet_type: \"Cat\"}"
     stacktrace:
       test/open_api_spex_one_of_demo/pet_plug_test.exs:19: (test)

I also have a version of the schemas using a Pet parent schema that can be found on the all-of branch:

defmodule OpenApiSpexOneOfDemo.Schemas do
  defmodule Pet do
    require OpenApiSpex

    alias OpenApiSpex.{Schema, Discriminator}

    OpenApiSpex.schema(%{
      title: "Pet",
      type: :object,
      properties: %{
        pet_type: %Schema{type: :string}
      },
      required: [:pet_type],
      discriminator: %Discriminator{
        propertyName: "pet_type"
      }
    })
  end

  defmodule Cat do
    require OpenApiSpex

    alias OpenApiSpex.Schema

    OpenApiSpex.schema(%{
      title: "Cat",
      type: :object,
      allOf: [
        Pet,
        %Schema{
          type: :object,
          properties: %{
            pet_type: %Schema{type: :string},
            meow: %Schema{type: :string}
          },
          required: [:meow]
        }
      ]
    })
  end

  defmodule Dog do
    require OpenApiSpex

    alias OpenApiSpex.Schema

    OpenApiSpex.schema(%{
      title: "Dog",
      type: :object,
      allOf: [
        Pet,
        %Schema{
          type: :object,
          properties: %{
            pet_type: %Schema{type: :string},
            bark: %Schema{type: :string}
          },
          required: [:bark]
        }
      ]
    })
  end
end

This schema results in the same behavior.


Thanks in advance for your help

Best โœจ

Potentially unsafe atom generation used in documentation

Within the docs the open_api_operation/1 function creates an atom :"#{action}_operation" which is invoked during the request execution.

This is a potential exploit vector (as atoms are not gc'd)

Sobelow highlighted this as follows:

##############################################
#                                            #
#          Running Sobelow - v0.7.7          #
#  Created by Griffin Byatt - @griffinbyatt  #
#     NCC Group - https://nccgroup.trust     #
#                                            #
##############################################

Unsafe atom interpolation - Low Confidence
File: apps/bond_web/lib/bond_web/schemas/integrations.ex - open_api_operation:99
Variable: action

I suggest the documentation is updated to have an example like the following:

    @spec open_api_operation(any) :: Operation.t()
    def open_api_operation(action) do
      operation = String.to_existing_atom("#{action}_operation")
      apply(__MODULE__, operation, [])
    end

Even better would be guidance on what to do if the operation is not defined, maybe a nice error message to aid the developer?

Missing path parameters go unnoticed

Using https://editor.swagger.io/ I just caught a few errors that so far had gone unnoticed. I had a few routes with path parameters (e.g. get("/dota2/abilities/:dota2_ability_id", Dota2.AbilityController, :show)) but I forgot to declare :dota2_ability_id in the parameters list. Here it is, commented out:

  @spec open_api_operation(atom()) :: Op.t()
  # /dota2/abilities/:dota2_ability_id
  def open_api_operation(:show),
    do: %Op{
    # parameters: [OpenAPI.param(:dota2_ability_id)],
      responses: OpenAPI.resp_Dota2Ability()
    }

Could we add some check that makes the ....OpenAPI.spec() call fail if path parameters are missing?
I'm not sure what could be done for missing parameters of other kinds.

How to use for a pure plug project? (no phoenix)

It's unclear to me if this is supposed to be usable in a pure plug project, which does not use phoenix. Will it work? If so how?

I've tried adapting the examples in the README but unsuccessfully...

Multiple content-type for req/resp header

Hi,

Trying to document an endpoint that accept and response to different content-type.
Example:

multi_content_type
multi_content_type_2

Either, response and request_body typedoc describes content: %{String.t => MediaType.t} | nil,

Any advice?
Many thanks in advance,

push a new version to hexdocs

Hello mbuhot,

thanks for the latest updates. They look very promising. Could you please release a new version on hexdocs to make them available to us? Or is there anything left to do before you can do so?

Greetings Thomas

JSON Api helpers

Hey there! Love this project! Im currently trying to migrate my teamโ€™s elixir swagger docs from swagger format 2.0 to 3.0. (All of the rest of our teams use OpenApi 3.0 in their Rails apps)

My project also conforms to JSON API Schema. The old tool we were using (phoenix_swagger) had JSONAPI helpers. Ex: https://hexdocs.pm/phoenix_swagger/json-api-helpers.html#content
They donโ€™t support openapi 3.0 though, which is why we want to start using this library. JSONAPI helpers to make writing these Schemas smoother would be a great addition.

Is that something that could be added to this project?

Dialyzer warnings when defining security schemes in spec

Hello @mbuhot ,

i already found another small problem. Basically, we are defining the spec just as you describe in the README.

defmodule UnifysellApiWeb.ApiSpec do
  alias OpenApiSpex.{OpenApi, Server, Info, Paths}

  @spec spec :: any
  def spec do
    %OpenApi{
      servers: [
        # Populate the Server info from a phoenix endpoint
        Server.from_endpoint(UnifysellApiWeb.Endpoint, otp_app: :unifysell_api)
      ],
      info: %Info{
        title: "UnifysellApi",
        version: "0.0.1"
      },
      # populate the paths from a phoenix router
      paths: Paths.from_router(UnifysellApiWeb.Router),
      components: %OpenApiSpex.Components{
        securitySchemes: %{
          OAuthBearer: %OpenApiSpex.SecurityScheme{
            type: "http",
            scheme: "bearer",
            bearerFormat: "JWT"
          }
        }
      }
    }
    # discover request/response schemas from path specs
    |> OpenApiSpex.resolve_schema_modules()
  end
end

We added a definition for an OAuth security scheme. The problem now is, that dialyzer is reporting the following warning: (i inserted the linebreak for better readability)

lib/unifysell_api_web/api_spec.ex:40: The call 'Elixir.OpenApiSpex':resolve_schema_modules
(#{'__struct__':='Elixir.OpenApiSpex.OpenApi', 'components':=#{'__struct__':='Elixir.OpenApiSpex.Components', 'callbacks':='nil', 'examples':='nil', 'headers':='nil', 'links':='nil', 'parameters':='nil', 'requestBodies':='nil', 'responses':='nil', 'schemas':='nil', 'securitySchemes':=#{'OAuthBearer2xx':=#{'__struct__':='Elixir.OpenApiSpex.SecurityScheme', 'bearerFormat':='nil', 'description':='nil', 'flows':='nil', 'in':=<<_:48>>, 'name':=<<_:104>>, 'openIdConnectUrl':='nil', 'scheme':='nil', 'type':=<<_:48>>}, 'OAuthBearer300':=#{'__struct__':='Elixir.OpenApiSpex.SecurityScheme', 'bearerFormat':=<<_:24>>, 'description':='nil', 'flows':='nil', 'in':='nil', 'name':='nil', 'openIdConnectUrl':='nil', 'scheme':=<<_:48>>, 'type':=<<_:32>>}}}, 'externalDocs':='nil', 'info':=#{'__struct__':='Elixir.OpenApiSpex.Info', 'contact':='nil', 'description':='nil', 'license':='nil', 'termsOfService':='nil', 'title':=<<_:96>>, 'version':=<<_:40>>}, 'openapi':=<<_:40>>, 'paths':=#{binary()=>#{'$ref':=binary(), '__struct__':='Elixir.OpenApiSpex.PathItem', 'delete':=#{'__struct__':='Elixir.OpenApiSpex.Operation', 'callbacks':='nil' | #{binary()=>#{'$ref'=>binary(), '__struct__'=>'Elixir.OpenApiSpex.Reference', binary()=>#{'$ref':=binary(), '__struct__':='Elixir.OpenApiSpex.PathItem', 'delete':=map(), 'description':=binary(), 'get':=map(), 'head':=map(), _=>_}}}, 'deprecated':='false' | 'nil' | 'true', 'description':='nil' | binary(), 'externalDocs':='nil' | #{'__struct__':='Elixir.OpenApiSpex.ExternalDocumentation', 'description':=binary(), 'url':=binary()}, 'operationId':='nil' | binary(), 'parameters':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Parameter' | 'Elixir.OpenApiSpex.Reference', '$ref'=>binary(), 'allowEmptyValue'=>boolean(), 'allowReserved'=>boolean(), 'content'=>map(), 'deprecated'=>boolean(), 'description'=>binary(), 'example'=>_, 'examples'=>map(), 'explode'=>boolean(), 'in'=>'cookie' | 'header' | 'path' | 'query', 'name'=>atom(), 'required'=>boolean(), 'schema'=>atom() | map(), 'style'=>'deep' | 'form' | 'label' | 'matrix' | 'pipeDelimited' | 'simple' | 'spaceDelimited'}], 'requestBody':='nil' | #{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.RequestBody', '$ref'=>binary(), 'content'=>#{binary()=>map()}, 'description'=>binary(), 'required'=>boolean()}, 'responses':=#{'default':=#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}, integer()=>#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}}, 'security':='nil' | [#{binary()=>[any()]}], 'servers':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Server', 'description':='nil' | binary(), 'url':=binary(), 'variables':=map()}], 'summary':='nil' | binary(), 'tags':='nil' | [binary()]}, 'description':=binary(), 'get':=#{'__struct__':='Elixir.OpenApiSpex.Operation', 'callbacks':='nil' | #{binary()=>#{'$ref'=>binary(), '__struct__'=>'Elixir.OpenApiSpex.Reference', binary()=>#{'$ref':=binary(), '__struct__':='Elixir.OpenApiSpex.PathItem', 'delete':=map(), 'description':=binary(), 'get':=map(), 'head':=map(), _=>_}}}, 'deprecated':='false' | 'nil' | 'true', 'description':='nil' | binary(), 'externalDocs':='nil' | #{'__struct__':='Elixir.OpenApiSpex.ExternalDocumentation', 'description':=binary(), 'url':=binary()}, 'operationId':='nil' | binary(), 'parameters':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Parameter' | 'Elixir.OpenApiSpex.Reference', '$ref'=>binary(), 'allowEmptyValue'=>boolean(), 'allowReserved'=>boolean(), 'content'=>map(), 'deprecated'=>boolean(), 'description'=>binary(), 'example'=>_, 'examples'=>map(), 'explode'=>boolean(), 'in'=>'cookie' | 'header' | 'path' | 'query', 'name'=>atom(), 'required'=>boolean(), 'schema'=>atom() | map(), 'style'=>'deep' | 'form' | 'label' | 'matrix' | 'pipeDelimited' | 'simple' | 'spaceDelimited'}], 'requestBody':='nil' | #{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.RequestBody', '$ref'=>binary(), 'content'=>#{binary()=>map()}, 'description'=>binary(), 'required'=>boolean()}, 'responses':=#{'default':=#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}, integer()=>#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}}, 'security':='nil' | [#{binary()=>[any()]}], 'servers':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Server', 'description':='nil' | binary(), 'url':=binary(), 'variables':=map()}], 'summary':='nil' | binary(), 'tags':='nil' | [binary()]}, 'head':=#{'__struct__':='Elixir.OpenApiSpex.Operation', 'callbacks':='nil' | #{binary()=>#{'$ref'=>binary(), '__struct__'=>'Elixir.OpenApiSpex.Reference', binary()=>#{'$ref':=binary(), '__struct__':='Elixir.OpenApiSpex.PathItem', 'delete':=map(), 'description':=binary(), 'get':=map(), 'head':=map(), _=>_}}}, 'deprecated':='false' | 'nil' | 'true', 'description':='nil' | binary(), 'externalDocs':='nil' | #{'__struct__':='Elixir.OpenApiSpex.ExternalDocumentation', 'description':=binary(), 'url':=binary()}, 'operationId':='nil' | binary(), 'parameters':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Parameter' | 'Elixir.OpenApiSpex.Reference', '$ref'=>binary(), 'allowEmptyValue'=>boolean(), 'allowReserved'=>boolean(), 'content'=>map(), 'deprecated'=>boolean(), 'description'=>binary(), 'example'=>_, 'examples'=>map(), 'explode'=>boolean(), 'in'=>'cookie' | 'header' | 'path' | 'query', 'name'=>atom(), 'required'=>boolean(), 'schema'=>atom() | map(), 'style'=>'deep' | 'form' | 'label' | 'matrix' | 'pipeDelimited' | 'simple' | 'spaceDelimited'}], 'requestBody':='nil' | #{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.RequestBody', '$ref'=>binary(), 'content'=>#{binary()=>map()}, 'description'=>binary(), 'required'=>boolean()}, 'responses':=#{'default':=#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}, integer()=>#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}}, 'security':='nil' | [#{binary()=>[any()]}], 'servers':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Server', 'description':='nil' | binary(), 'url':=binary(), 'variables':=map()}], 'summary':='nil' | binary(), 'tags':='nil' | [binary()]}, 'options':=#{'__struct__':='Elixir.OpenApiSpex.Operation', 'callbacks':='nil' | #{binary()=>#{'$ref'=>binary(), '__struct__'=>'Elixir.OpenApiSpex.Reference', binary()=>#{'$ref':=binary(), '__struct__':='Elixir.OpenApiSpex.PathItem', 'delete':=map(), 'description':=binary(), 'get':=map(), 'head':=map(), _=>_}}}, 'deprecated':='false' | 'nil' | 'true', 'description':='nil' | binary(), 'externalDocs':='nil' | #{'__struct__':='Elixir.OpenApiSpex.ExternalDocumentation', 'description':=binary(), 'url':=binary()}, 'operationId':='nil' | binary(), 'parameters':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Parameter' | 'Elixir.OpenApiSpex.Reference', '$ref'=>binary(), 'allowEmptyValue'=>boolean(), 'allowReserved'=>boolean(), 'content'=>map(), 'deprecated'=>boolean(), 'description'=>binary(), 'example'=>_, 'examples'=>map(), 'explode'=>boolean(), 'in'=>'cookie' | 'header' | 'path' | 'query', 'name'=>atom(), 'required'=>boolean(), 'schema'=>atom() | map(), 'style'=>'deep' | 'form' | 'label' | 'matrix' | 'pipeDelimited' | 'simple' | 'spaceDelimited'}], 'requestBody':='nil' | #{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.RequestBody', '$ref'=>binary(), 'content'=>#{binary()=>map()}, 'description'=>binary(), 'required'=>boolean()}, 'responses':=#{'default':=#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}, integer()=>#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}}, 'security':='nil' | [#{binary()=>[any()]}], 'servers':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Server', 'description':='nil' | binary(), 'url':=binary(), 'variables':=map()}], 'summary':='nil' | binary(), 'tags':='nil' | [binary()]}, 'parameters':=[#{'__struct__':='Elixir.OpenApiSpex.Parameter' | 'Elixir.OpenApiSpex.Reference', '$ref'=>binary(), 'allowEmptyValue'=>boolean(), 'allowReserved'=>boolean(), 'content'=>#{binary()=>map()}, 'deprecated'=>boolean(), 'description'=>binary(), 'example'=>_, 'examples'=>#{binary()=>map()}, 'explode'=>boolean(), 'in'=>'cookie' | 'header' | 'path' | 'query', 'name'=>atom(), 'required'=>boolean(), 'schema'=>atom() | #{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Schema', '$ref'=>binary(), 'additionalProperties'=>atom() | map(), 'allOf'=>'nil' | [any()], 'anyOf'=>'nil' | [any()], 'default'=>_, 'deprecated'=>'false' | 'nil' | 'true', 'description'=>binary(), 'discriminator'=>'nil' | map(), 'enum'=>'nil' | [any()], 'example'=>_, 'exclusiveMaximum'=>'false' | 'nil' | 'true', 'exclusiveMinimum'=>'false' | 'nil' | 'true', 'externalDocs'=>'nil' | map(), 'format'=>'nil' | binary(), 'items'=>atom() | map(), 'maxItems'=>'nil' | integer(), 'maxLength'=>'nil' | integer(), 'maxProperties'=>'nil' | integer(), 'maximum'=>'nil' | number(), 'minItems'=>'nil' | integer(), 'minLength'=>'nil' | integer(), 'minProperties'=>'nil' | integer(), 'minimum'=>'nil' | number(), 'multipleOf'=>'nil' | number(), 'not'=>atom() | map(), 'nullable'=>'false' | 'nil' | 'true', 'oneOf'=>'nil' | [any()], 'pattern'=>'nil' | binary() | map(), 'properties'=>'nil' | map(), 'readOnly'=>'false' | 'nil' | 'true', 'required'=>'nil' | [any()], 'title'=>binary(), 'type'=>atom(), 'uniqueItems'=>'false' | 'nil' | 'true', 'writeOnly'=>'false' | 'nil' | 'true', 'x-struct'=>atom(), 'xml'=>'nil' | map()}, 'style'=>'deep' | 'form' | 'label' | 'matrix' | 'pipeDelimited' | 'simple' | 'spaceDelimited'}], 'patch':=#{'__struct__':='Elixir.OpenApiSpex.Operation', 'callbacks':='nil' | #{binary()=>#{'$ref'=>binary(), '__struct__'=>'Elixir.OpenApiSpex.Reference', binary()=>#{'$ref':=binary(), '__struct__':='Elixir.OpenApiSpex.PathItem', 'delete':=map(), 'description':=binary(), 'get':=map(), 'head':=map(), _=>_}}}, 'deprecated':='false' | 'nil' | 'true', 'description':='nil' | binary(), 'externalDocs':='nil' | #{'__struct__':='Elixir.OpenApiSpex.ExternalDocumentation', 'description':=binary(), 'url':=binary()}, 'operationId':='nil' | binary(), 'parameters':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Parameter' | 'Elixir.OpenApiSpex.Reference', '$ref'=>binary(), 'allowEmptyValue'=>boolean(), 'allowReserved'=>boolean(), 'content'=>map(), 'deprecated'=>boolean(), 'description'=>binary(), 'example'=>_, 'examples'=>map(), 'explode'=>boolean(), 'in'=>'cookie' | 'header' | 'path' | 'query', 'name'=>atom(), 'required'=>boolean(), 'schema'=>atom() | map(), 'style'=>'deep' | 'form' | 'label' | 'matrix' | 'pipeDelimited' | 'simple' | 'spaceDelimited'}], 'requestBody':='nil' | #{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.RequestBody', '$ref'=>binary(), 'content'=>#{binary()=>map()}, 'description'=>binary(), 'required'=>boolean()}, 'responses':=#{'default':=#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}, integer()=>#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}}, 'security':='nil' | [#{binary()=>[any()]}], 'servers':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Server', 'description':='nil' | binary(), 'url':=binary(), 'variables':=map()}], 'summary':='nil' | binary(), 'tags':='nil' | [binary()]}, 'post':=#{'__struct__':='Elixir.OpenApiSpex.Operation', 'callbacks':='nil' | #{binary()=>#{'$ref'=>binary(), '__struct__'=>'Elixir.OpenApiSpex.Reference', binary()=>#{'$ref':=binary(), '__struct__':='Elixir.OpenApiSpex.PathItem', 'delete':=map(), 'description':=binary(), 'get':=map(), 'head':=map(), _=>_}}}, 'deprecated':='false' | 'nil' | 'true', 'description':='nil' | binary(), 'externalDocs':='nil' | #{'__struct__':='Elixir.OpenApiSpex.ExternalDocumentation', 'description':=binary(), 'url':=binary()}, 'operationId':='nil' | binary(), 'parameters':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Parameter' | 'Elixir.OpenApiSpex.Reference', '$ref'=>binary(), 'allowEmptyValue'=>boolean(), 'allowReserved'=>boolean(), 'content'=>map(), 'deprecated'=>boolean(), 'description'=>binary(), 'example'=>_, 'examples'=>map(), 'explode'=>boolean(), 'in'=>'cookie' | 'header' | 'path' | 'query', 'name'=>atom(), 'required'=>boolean(), 'schema'=>atom() | map(), 'style'=>'deep' | 'form' | 'label' | 'matrix' | 'pipeDelimited' | 'simple' | 'spaceDelimited'}], 'requestBody':='nil' | #{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.RequestBody', '$ref'=>binary(), 'content'=>#{binary()=>map()}, 'description'=>binary(), 'required'=>boolean()}, 'responses':=#{'default':=#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}, integer()=>#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}}, 'security':='nil' | [#{binary()=>[any()]}], 'servers':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Server', 'description':='nil' | binary(), 'url':=binary(), 'variables':=map()}], 'summary':='nil' | binary(), 'tags':='nil' | [binary()]}, 'put':=#{'__struct__':='Elixir.OpenApiSpex.Operation', 'callbacks':='nil' | #{binary()=>#{'$ref'=>binary(), '__struct__'=>'Elixir.OpenApiSpex.Reference', binary()=>#{'$ref':=binary(), '__struct__':='Elixir.OpenApiSpex.PathItem', 'delete':=map(), 'description':=binary(), 'get':=map(), 'head':=map(), _=>_}}}, 'deprecated':='false' | 'nil' | 'true', 'description':='nil' | binary(), 'externalDocs':='nil' | #{'__struct__':='Elixir.OpenApiSpex.ExternalDocumentation', 'description':=binary(), 'url':=binary()}, 'operationId':='nil' | binary(), 'parameters':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Parameter' | 'Elixir.OpenApiSpex.Reference', '$ref'=>binary(), 'allowEmptyValue'=>boolean(), 'allowReserved'=>boolean(), 'content'=>map(), 'deprecated'=>boolean(), 'description'=>binary(), 'example'=>_, 'examples'=>map(), 'explode'=>boolean(), 'in'=>'cookie' | 'header' | 'path' | 'query', 'name'=>atom(), 'required'=>boolean(), 'schema'=>atom() | map(), 'style'=>'deep' | 'form' | 'label' | 'matrix' | 'pipeDelimited' | 'simple' | 'spaceDelimited'}], 'requestBody':='nil' | #{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.RequestBody', '$ref'=>binary(), 'content'=>#{binary()=>map()}, 'description'=>binary(), 'required'=>boolean()}, 'responses':=#{'default':=#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}, integer()=>#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}}, 'security':='nil' | [#{binary()=>[any()]}], 'servers':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Server', 'description':='nil' | binary(), 'url':=binary(), 'variables':=map()}], 'summary':='nil' | binary(), 'tags':='nil' | [binary()]}, 'servers':=[#{'__struct__':='Elixir.OpenApiSpex.Server', 'description':='nil' | binary(), 'url':=binary(), 'variables':=#{binary()=>map()}}], 'summary':=binary(), 'trace':=#{'__struct__':='Elixir.OpenApiSpex.Operation', 'callbacks':='nil' | #{binary()=>#{'$ref'=>binary(), '__struct__'=>'Elixir.OpenApiSpex.Reference', binary()=>#{'$ref':=binary(), '__struct__':='Elixir.OpenApiSpex.PathItem', 'delete':=map(), 'description':=binary(), 'get':=map(), 'head':=map(), _=>_}}}, 'deprecated':='false' | 'nil' | 'true', 'description':='nil' | binary(), 'externalDocs':='nil' | #{'__struct__':='Elixir.OpenApiSpex.ExternalDocumentation', 'description':=binary(), 'url':=binary()}, 'operationId':='nil' | binary(), 'parameters':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Parameter' | 'Elixir.OpenApiSpex.Reference', '$ref'=>binary(), 'allowEmptyValue'=>boolean(), 'allowReserved'=>boolean(), 'content'=>map(), 'deprecated'=>boolean(), 'description'=>binary(), 'example'=>_, 'examples'=>map(), 'explode'=>boolean(), 'in'=>'cookie' | 'header' | 'path' | 'query', 'name'=>atom(), 'required'=>boolean(), 'schema'=>atom() | map(), 'style'=>'deep' | 'form' | 'label' | 'matrix' | 'pipeDelimited' | 'simple' | 'spaceDelimited'}], 'requestBody':='nil' | #{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.RequestBody', '$ref'=>binary(), 'content'=>#{binary()=>map()}, 'description'=>binary(), 'required'=>boolean()}, 'responses':=#{'default':=#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}, integer()=>#{'__struct__':='Elixir.OpenApiSpex.Reference' | 'Elixir.OpenApiSpex.Response', '$ref'=>binary(), 'content'=>map(), 'description'=>binary(), 'headers'=>'nil' | map(), 'links'=>'nil' | map()}}, 'security':='nil' | [#{binary()=>[any()]}], 'servers':='nil' | [#{'__struct__':='Elixir.OpenApiSpex.Server', 'description':='nil' | binary(), 'url':=binary(), 'variables':=map()}], 'summary':='nil' | binary(), 'tags':='nil' | [binary()]}}}, 'security':=[], 'servers':=[#{'__struct__':='Elixir.OpenApiSpex.Server', 'description':='nil', 'url':=<<_:32,_:_*8>>, 'variables':=#{}},...], 'tags':=[]})
will never return since it differs in the 1st argument from the success typing arguments:
(#{'__struct__':='Elixir.OpenApiSpex.OpenApi', 'components':='nil' | #{'__struct__':='Elixir.OpenApiSpex.Components', 'callbacks':=#{binary()=>map()}, 'examples':=#{binary()=>map()}, 'headers':=#{binary()=>map()}, 'links':=#{binary()=>map()}, 'parameters':=#{binary()=>map()}, 'requestBodies':=#{binary()=>map()}, 'responses':=#{binary()=>map()}, 'schemas':=#{binary()=>map()}, 'securitySchemes':=#{binary()=>map()}}, 'externalDocs':='nil' | #{'__struct__':='Elixir.OpenApiSpex.ExternalDocumentation', 'description':=binary(), 'url':=binary()}, 'info':=#{'__struct__':='Elixir.OpenApiSpex.Info', 'contact':='nil' | #{'__struct__':='Elixir.OpenApiSpex.Contact', 'email':=binary(), 'name':=binary(), 'url':=binary()}, 'description':='nil' | binary(), 'license':='nil' | #{'__struct__':='Elixir.OpenApiSpex.License', 'name':=binary(), 'url':=binary()}, 'termsOfService':='nil' | binary(), 'title':=binary(), 'version':=binary()}, 'openapi':=binary(), 'paths':=#{binary()=>#{'$ref':=binary(), '__struct__':='Elixir.OpenApiSpex.PathItem', 'delete':=map(), 'description':=binary(), 'get':=map(), 'head':=map(), 'options':=map(), 'parameters':=[any()], 'patch':=map(), 'post':=map(), 'put':=map(), 'servers':=[any()], 'summary':=binary(), 'trace':=map()}}, 'security':=[#{binary()=>[any()]}], 'servers':=[#{'__struct__':='Elixir.OpenApiSpex.Server', 'description':='nil' | binary(), 'url':=binary(), 'variables':=map()}], 'tags':=[#{'__struct__':='Elixir.OpenApiSpex.Tag', 'description':=binary(), 'externalDocs':=map(), 'name':=binary()}]})

The problem is, that we are inserting an incomplete %OpenApiSpex.Components{} struct, that is merged with the schemas definition later. Do you have any ideas how we could handle that?

Map-access on schema when using as request body on validation

Hello again @mbuhot ,

we are running into a problem using the the validation plug.

We are defining a schema as request body:

  def mark_as_shipped_operation do
    %Operation{
      ...
      requestBody:
        Operation.request_body(
          "the request body object",
          "application/json",
          Schemas.OrderMarkAsShippedRequest,
          required: false
        ),
      ...
    }
  end

While the validate plug expects a map, it gets a struct in this situation. This leads to the following error, since struct[key] access is not possible on a struct:

test update order valid mark order shipped (UnifysellApiWeb.OrderControllerTest)
     test/unifysell_api_web/controllers/order_controller_test.exs:79
     ** (UndefinedFunctionError) function UnifysellApiWeb.Schemas.OrderMarkAsShippedRequest.fetch/2 is undefined (UnifysellApiWeb.Schemas.OrderMarkAsShippedRequest does not implement the Access behaviour)
     code: conn = put conn, "/api/order/1/mark-as-shipped", ~S({"labelId": 1})
     stacktrace:
       (unifysell_api) UnifysellApiWeb.Schemas.OrderMarkAsShippedRequest.fetch(%{__struct__: UnifysellApiWeb.Schemas.OrderMarkAsShippedRequest, labelId: 1, order_id: 1}, :order_id)
       (elixir) lib/access.ex:308: Access.get/3
       (open_api_spex) lib/open_api_spex/operation.ex:179: OpenApiSpex.Operation.validate_parameter_schemas/3
       (open_api_spex) lib/open_api_spex/operation.ex:156: OpenApiSpex.Operation.validate/4
       (open_api_spex) lib/open_api_spex/plug/validate.ex:33: OpenApiSpex.Plug.Validate.call/2
       (unifysell_api) lib/unifysell_api_web/controllers/order_controller.ex:1: UnifysellApiWeb.OrderController.phoenix_controller_pipeline/2
       (unifysell_api) lib/unifysell_api_web/endpoint.ex:1: UnifysellApiWeb.Endpoint.instrument/4
       (phoenix) lib/phoenix/router.ex:278: Phoenix.Router.__call__/1
       (unifysell_api) lib/unifysell_api_web/endpoint.ex:1: UnifysellApiWeb.Endpoint.plug_builder_call/2
       (unifysell_api) lib/unifysell_api_web/endpoint.ex:1: UnifysellApiWeb.Endpoint.call/2
       (phoenix) lib/phoenix/test/conn_test.ex:224: Phoenix.ConnTest.dispatch/5
       test/unifysell_api_web/controllers/order_controller_test.exs:93: (test)

Updating schema works, but of course is not a good solution, But we get an idea why the error occurs.:

defmodule OrderMarkAsShippedRequest do
    def fetch(a, b) do
      Map.fetch(a, b)
    end

    OpenApiSpex.schema(%{...})
  end

We could update the validate_parameter_schemas(...) function like this:

  @spec validate_parameter_schemas([Parameter.t], map, %{String.t => Schema.t}) :: {:ok, map} | {:error, String.t}
  defp validate_parameter_schemas([], params, _schemas), do: {:ok, params}
  defp validate_parameter_schemas(param_list, %_{} = params, schemas) do
    validate_parameter_schemas(param_list, Map.from_struct(params), schemas)
  end
  defp validate_parameter_schemas([p | rest], %{} = params, schemas) do
    with :ok <- Schema.validate(Parameter.schema(p), params[p.name], schemas),
         {:ok, remaining} <- validate_parameter_schemas(rest, params, schemas) do
      {:ok, Map.delete(remaining, p.name)}
    end
  end

We could also update it like this:

  @spec validate_parameter_schemas([Parameter.t], map, %{String.t => Schema.t}) :: {:ok, map} | {:error, String.t}
  defp validate_parameter_schemas([], params, _schemas), do: {:ok, params}
  defp validate_parameter_schemas([p | rest], %{} = params, schemas) do
    with :ok <- Schema.validate(Parameter.schema(p), Map.fetch(params, p.name), schemas),
         {:ok, remaining} <- validate_parameter_schemas(rest, params, schemas) do
      {:ok, Map.delete(remaining, p.name)}
    end
  end

If one of these solutions is okay to you, just tell me and i will create a pull request. Or maybe we are doing something else wrong?

Greetings, Thomas

cast does not "atomify" keys in maps of enum payloads

From #42

enums validation: cast does not "atomify" keys in maps of enum payloads. In other words, the following code does not crash:

schema = %Schema{enum: [%{id: 42}]}
spec = ...
# string keys
{:ok, data} = OpenApiSpex.cast(spec, schema, %{"id" => 42})
{:error, _} = OpenApiSpex.validate(spec, schema, data)
# atom keys
{:ok, data} = OpenApiSpex.cast(spec, schema, %{id: 42})
:ok = OpenApiSpex.validate(spec, schema, data)

I think it would be very dangerous (OOM) to atomify enum keys anyway!
My current workaround is to specify enum schemas with string keys instead, which can be easily missed by anyone.

So how about throwing an error when creating a schema that has atom keys in enum: ...?

Response Serialization

Instead of manually implementing endpoint response serialization, why not leverage OpenApiSpex to do it? We've been using ja_serializer for our json:api endpoints, but it's kind of a pain, and it duplicates what's already in our OpenApiSpex specifications.

Casting allOf: will result in a :unexpected_field error

Hey There,
It looks like there is an issue with the allOf casting mechanism.

The main issue is, that the caster will return the error :unexpected_field since the payload will mostly provide some files which are not defined in every schema of allOf.

As an example:

schema = %Schema{
  title: "demo",
  type: :object,
  allOf: [
      MyApp.Schemas.GenericRequest,
      %Schema{
          properties: %{
               bar: %Schema{type: :string}
          }
      }
  ]
}

Let's just assume that GenericRequest has something defined like id: %Schema{type: string} and the request looks something like this: {"id": "1231-123155-1231-12312", "bar": "whatever"}

Based on https://github.com/open-api-spex/open_api_spex/blob/master/lib/open_api_spex/cast/all_of.ex#L8, the caster will first try to cast everything from the request on the first schema, but since I've only defined the id and not the bar in the first one, this will always result in a :unexpected_field error

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.