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] Openapi generated docs won't submit arrays correctly in forms. #1289

Open
KonstantinosPetrakis opened this issue Aug 30, 2024 · 2 comments

Comments

@KonstantinosPetrakis
Copy link

KonstantinosPetrakis commented Aug 30, 2024

Greetings. There's been an issue to the problem I am referring to, which was closed for no reason to my understanding.

Essentially, when you define a Schema with a List[str] field the openapi generated documentation is not in sync with what the schema excepts.

The schema excepts a repeated value (e.g names=John&names=Jane) or through JavaScript code:

const names = ["John", "Jane"];
const form = new FormData();
for (const name of names) form.append("names", name);
// make request

But the openapi docs send a single comma separated string resulting to an array with a single item containing that string (e.g ["John, Jane"].

The documentation is supposed to work out of the box. User code checking for commas to fix that library issue shouldn't be acceptable (what if the separated string contained a comma itself?).

@KonstantinosPetrakis
Copy link
Author

As the author of the original issue mentioned it most likely has to do something with explode:false.

@KonstantinosPetrakis
Copy link
Author

There's a dirty monkey patch you can use (the js code was written by ChatGPT because it was a really tedious task).

Essentially, you fetch the generated openapi schema from ninja api, and you make any form field or query parameter use style: form and explode: true.

Here's how to apply it:

  1. You write a custom DocsBase to render your template and use it in the NinjaAPI:
from ninja.openapi.docs import DocsBase

class CustomSwagger(DocsBase):
    def render_page(self, request, api):
        return render(request, "swagger.html")

api = NinjaAPI(title="My API", docs=CustomSwagger())
  1. You paste the following in your template swagger.html:
<!DOCTYPE html>
<html>
  <head>
    <link
      type="text/css"
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css"
    />
    <link rel="shortcut icon" href="/media/logo.png" />
    <title>My API</title>
  </head>
  <body>
    <div id="swagger-ui"></div>
    <script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
    <script>
      function transformOpenAPI(openApiObject) {
        // Iterate through paths in the OpenAPI object
        for (const path in openApiObject.paths) {
          for (const method in openApiObject.paths[path]) {
            const operation = openApiObject.paths[path][method];

            // Transform query parameters for GET requests
            if (method.toLowerCase() === "get" && operation.parameters) {
              operation.parameters.forEach((param) => {
                if (
                  param.in === "query" &&
                  param.schema &&
                  param.schema.type === "array"
                ) {
                  param.style = "form";
                  param.explode = true;
                }
              });
            }

            // Transform body for application/x-www-form-urlencoded
            if (operation.requestBody) {
              const content = operation.requestBody.content;

              // Check for application/x-www-form-urlencoded
              if (content["application/x-www-form-urlencoded"]) {
                const formEncoding =
                  content["application/x-www-form-urlencoded"];

                // Assuming the schema is an object with properties
                if (
                  formEncoding.schema.type === "object" &&
                  formEncoding.schema.properties
                ) {
                  Object.keys(formEncoding.schema.properties).forEach(
                    (prop) => {
                      const property = formEncoding.schema.properties[prop];

                      if (property.type === "array") {
                        // Set encoding for array properties
                        formEncoding.encoding = formEncoding.encoding || {};
                        formEncoding.encoding[prop] = {
                          style: "form",
                          explode: true,
                        };
                      }
                    }
                  );
                }
              }
            }
          }
        }

        return openApiObject;
      }

      (async () => {
        const r = await fetch("/api/openapi.json");
        const openapi = await r.json();
        transformOpenAPI(openapi);

        const ui = SwaggerUIBundle({
          layout: "BaseLayout",
          deepLinking: true,
          spec: openapi,
          dom_id: "#swagger-ui",
          presets: [
            SwaggerUIBundle.presets.apis,
            SwaggerUIBundle.SwaggerUIStandalonePreset,
          ],
        });
      })();
    </script>
  </body>
</html>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant