vitalik / django-ninja

💨 Fast, Async-ready, Openapi, type hints based framework for building APIs
https://django-ninja.dev
MIT License
7.36k stars 437 forks source link

[BUG] Openapi generated docs won't submit arrays correctly in forms. #1289

Open KonstantinosPetrakis opened 3 months ago

KonstantinosPetrakis commented 3 months ago

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 commented 3 months ago

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

KonstantinosPetrakis commented 1 month ago

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())

2. You paste the following in your template `swagger.html`:
```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>