OpenAPI errors

Fix application/x-www-form-urlencoded request bodies mis-generated by OpenAPI codegen

A application/x-www-form-urlencoded request body with a nested object or array property validates fine and generates a clean-looking TypeScript client — then serializes that property to the literal string [object Object] or [object Blob] on the wire. We reproduced this directly: openapi-generator's typescript-fetch and typescript-axios templates both mis-serialize non-flat properties in a urlencoded body, and no validator flags the spec as a problem because it's structurally valid. The fix is to keep urlencoded bodies flat, or move nested data to application/json. Reproduced with real tools on 2026-09-21.

How do you reproduce the error?

Take a token-request body with a flat string, an array, and a nested object — a realistic shape if a metadata field crept in over time:

requestBody:
  content:
    application/x-www-form-urlencoded:
      schema:
        type: object
        properties:
          grant_type: { type: string }
          client_id: { type: string }
          scopes:
            type: array
            items: { type: string }
          metadata:
            type: object
            properties:
              source: { type: string }
        required: [grant_type, client_id]

redocly lint v2.53.3 against this spec reports nothing about the nested metadata or scopes properties — only unrelated warnings (missing servers, missing 4XX response). The schema is structurally valid JSON Schema; nothing in the urlencoded content type constrains what shape properties inside it can have.

Generating a client with openapi-generator-cli generate -g typescript-fetch produces this in the request-building code:

if (requestParameters['scopes'] != null) {
    formParams.append('scopes', requestParameters['scopes']!.join(runtime.COLLECTION_FORMATS["csv"]));
}
if (requestParameters['metadata'] != null) {
    formParams.append('metadata', new Blob([JSON.stringify(CreateTokenRequestMetadataToJSON(requestParameters['metadata']))], { type: "application/json" }));
}

We ran the equivalent serialization directly in Node to see the actual body that goes over the wire:

const formParams = new URLSearchParams();
formParams.append('grant_type', 'client_credentials');
formParams.append('scopes', ['read:messages','write:messages'].join(','));
formParams.append('metadata', new Blob([JSON.stringify({source:'cli'})], {type:'application/json'}));
console.log(formParams.toString());
grant_type=client_credentials&scopes=read%3Amessages%2Cwrite%3Amessages&metadata=%5Bobject+Blob%5D

Decoded, that last part is metadata=[object Blob] — the literal string, not the JSON payload. URLSearchParams.append() coerces its second argument to a string, and String(new Blob(...)) is "[object Blob]". The typescript-axios generator has the same problem with a different failure string: it calls localVarFormParams.set('metadata', metadata as any) with the raw object, and String({source: 'cli'}) produces [object Object]. We confirmed this with the same Node substitution.

Why does this happen?

Per the OpenAPI 3.1.1 specification, the Encoding Object exists specifically to control how non-primitive schema properties in a urlencoded (or multipart) body get serialized, and its style/explode fields default to style: form, explode: true for application/x-www-form-urlencoded bodies. Per the spec, explode: true on an array property should generate one repeated key per value (scopes=read%3Amessages&scopes=write%3Amessages), not a single comma-joined field — but both generators we tested always join arrays with a comma regardless of the explode default, which is already a deviation from the documented default behavior. Nested objects have no defined form-urlencoded serialization at all in either the OpenAPI or the underlying application/x-www-form-urlencoded (WHATWG URL) spec — x-www-form-urlencoded is fundamentally a flat key-value format. Neither generator template treats "no defined serialization" as an error; both fall through to whatever String(value) happens to produce for the runtime object they're holding — a Blob in one generator's implementation, a plain object in the other's.

How do you fix it?

Keep application/x-www-form-urlencoded bodies flat — primitives and arrays of primitives only — and move anything nested to JSON:

# Before — metadata can't be represented in form-urlencoded at all
requestBody:
  content:
    application/x-www-form-urlencoded:
      schema:
        type: object
        properties:
          grant_type: { type: string }
          metadata:
            type: object
            properties:
              source: { type: string }

# After — flatten what you can, move the rest to a JSON body/endpoint
requestBody:
  content:
    application/x-www-form-urlencoded:
      schema:
        type: object
        properties:
          grant_type: { type: string }
          metadata_source: { type: string }

If the endpoint genuinely needs nested structure and you don't control the content type (some OAuth2 token endpoints, for instance, require urlencoded bodies per spec), don't rely on generated client code to serialize it correctly — write the body construction by hand for that one call and skip the generated form-serialization path entirely.

How different tools react

Tool Flat urlencoded body Nested object/array property in urlencoded body
swagger-cli validate (AJV) Valid Valid — no urlencoded-specific check exists
redocly lint (semantic linter) Valid (plus unrelated style warnings) Valid — same, no structural error
openapi-generator typescript-fetch Correct URLSearchParams body Compiles; array CSV-joined, object serialized as [object Blob]
openapi-generator typescript-axios Correct URLSearchParams body Compiles; array CSV-joined, object serialized as [object Object]
Runtime Correct request body Wrong data reaches the server — often a silent 400 or a field that's just ignored

The practical risk is that this compiles cleanly and passes every validator we tested — tsc never sees a type error because the generated code casts to any at the append/set call, and no OpenAPI linter treats "this property has no urlencoded serialization" as invalid. It only shows up when you inspect the actual request body, in a network tab or an integration test against a real server.

How to catch this before it ships

  1. Don't rely on a validator to catch this — as shown above, both swagger-cli and redocly lint accept nested properties in a urlencoded body without complaint. Review application/x-www-form-urlencoded schemas by eye for anything beyond primitives and arrays of primitives.
  2. Test the actual serialized body, not just that the generated method compiles. Log or intercept the outgoing request in an integration test and assert on the literal string, the way we did with formParams.toString() above.
  3. For OAuth2 token endpoints and other places you're required to use urlencoded bodies, treat that one request as worth hand-writing rather than trusting default codegen — see the OAuth2 codegen post for the related gap in generated auth flows.

Sourced's SDK preview lets you inspect the actual generated request-building code for every operation before it ships, so a urlencoded body with a nested property that would serialize to [object Object] is visible in the preview, not discovered in production. Create hosted docs from your repo or start a free report to see your generated request bodies before you publish.

FAQ

Does this affect application/json request bodies too?

No — this is specific to application/x-www-form-urlencoded (and, similarly, multipart/form-data — see the multipart file upload post for that failure mode). JSON bodies serialize nested objects and arrays natively via JSON.stringify, with no equivalent gap.

Why doesn't redocly lint flag a nested object in a urlencoded body?

Because it's checking the spec's structural and semantic correctness as JSON Schema and OpenAPI — a nested object is a perfectly valid Schema Object regardless of content type. Nothing in the OpenAPI spec says a urlencoded media type's schema can't contain nested objects; it's the WHATWG application/x-www-form-urlencoded format itself that has no defined way to represent one, and that's outside what a spec linter checks.

Does the Encoding Object's style/explode fields fix this?

They control array and object parameter serialization style (form, spaceDelimited, pipeDelimited, deepObject) for cases the format can represent, but they don't make a nested object encodable in application/x-www-form-urlencoded — and as shown above, the generators we tested didn't honor the explode: true default for arrays either. Don't assume setting style/explode explicitly changes the generated output without testing it against your specific generator.

Is this specific to openapi-generator, or do other codegen tools have the same problem?

We tested openapi-generator's typescript-fetch and typescript-axios templates specifically. Other generators may handle the flat case correctly and still lack any defined behavior for nested properties, since the gap is really in the application/x-www-form-urlencoded format itself, not one tool's implementation — treat any generator's handling of nested urlencoded properties as unverified until you test it.

What's the safest content type for a request body with nested data?

application/json, if the API you're calling accepts it. Urlencoded bodies exist mainly for HTML form compatibility and specific protocols (OAuth2 token requests) that require them — use them only where required, and keep the schema flat.