|
3 | 3 | ⚠️ This README may be for the latest development version, which may |
4 | 4 | contain unreleased changes. Please ensure you're looking at the README for the latest release version. |
5 | 5 |
|
6 | | -This provides any runtime-specific code that the generated code that oapi-codegen generates may need, and therefore is expected to be used with [deepmap/oapi-codegen](https://github.com/deepmap/oapi-codegen). |
| 6 | +This package provides helper functions for marshaling and unmarshalling HTTP |
| 7 | +parameters in headers, cookies, and query arguments in various formats, as well |
| 8 | +as functions for reading and writing form encoded data representing models. |
| 9 | + |
| 10 | +You won't need to use this package directly, since it's imported be the boilerplate |
| 11 | +from `oapi-codegen`, however, you do need to use the correct version needed by |
| 12 | +the code generator, since it makes assumptions about runtime behavior. |
| 13 | + |
| 14 | +## Parameter Handling |
| 15 | + |
| 16 | +OpenAPI 3.x parameters are characterized by three orthogonal attributes: |
| 17 | +**style**, **location**, and **explode**. The serialized form on the wire is |
| 18 | +determined by the combination of all three. |
| 19 | + |
| 20 | +### Styles |
| 21 | + |
| 22 | +Parameters come in the following styles (all defined by the OpenAPI 3.x spec): |
| 23 | + |
| 24 | +- **`simple`** — comma-separated values. The default for path and header |
| 25 | + parameters. |
| 26 | +- **`label`** — values prefixed with `.`, separated by `.` (explode) or `,` |
| 27 | + (no explode). Path parameters only. |
| 28 | +- **`matrix`** — values prefixed with `;name=`, repeated (explode) or |
| 29 | + comma-separated (no explode). Path parameters only. |
| 30 | +- **`form`** — `name=value` pairs joined with `&`. The default for query and |
| 31 | + cookie parameters. |
| 32 | +- **`spaceDelimited`** — array elements joined by literal spaces (no |
| 33 | + explode); behaves identically to `form` when exploded. Query parameters, |
| 34 | + arrays only. |
| 35 | +- **`pipeDelimited`** — array elements joined by literal `|` (no explode); |
| 36 | + behaves identically to `form` when exploded. Query parameters, arrays |
| 37 | + only. |
| 38 | +- **`deepObject`** — nested bracket notation, e.g. `name[field]=value`. |
| 39 | + Query parameters, objects only, must be exploded. |
| 40 | + |
| 41 | +### Locations |
| 42 | + |
| 43 | +Each style is only valid in specific parameter locations: |
| 44 | + |
| 45 | +| Location | Allowed styles | |
| 46 | +|----------|---------------| |
| 47 | +| `path` | `simple`, `label`, `matrix` | |
| 48 | +| `query` | `form`, `spaceDelimited`, `pipeDelimited`, `deepObject` | |
| 49 | +| `header` | `simple` | |
| 50 | +| `cookie` | `form` | |
| 51 | + |
| 52 | +### Explode |
| 53 | + |
| 54 | +Each style can be `explode: true` or `explode: false`, which changes how |
| 55 | +multi-value parameters (arrays and objects) are packed. |
| 56 | + |
| 57 | +<table> |
| 58 | +<thead> |
| 59 | +<tr> |
| 60 | +<th>Style</th><th>Type</th><th><code>explode: false</code></th><th><code>explode: true</code></th> |
| 61 | +</tr> |
| 62 | +</thead> |
| 63 | +<tbody> |
| 64 | +<tr><td rowspan="3"><code>simple</code></td> |
| 65 | + <td>primitive <code>5</code></td><td><code>5</code></td><td><code>5</code></td></tr> |
| 66 | +<tr><td>array <code>[3,4,5]</code></td><td><code>3,4,5</code></td><td><code>3,4,5</code></td></tr> |
| 67 | +<tr><td>object <code>{role:"admin", firstName:"Alex"}</code></td><td><code>firstName,Alex,role,admin</code></td><td><code>firstName=Alex,role=admin</code></td></tr> |
| 68 | + |
| 69 | +<tr><td rowspan="3"><code>label</code></td> |
| 70 | + <td>primitive <code>5</code></td><td><code>.5</code></td><td><code>.5</code></td></tr> |
| 71 | +<tr><td>array <code>[3,4,5]</code></td><td><code>.3,4,5</code></td><td><code>.3.4.5</code></td></tr> |
| 72 | +<tr><td>object <code>{role:"admin", firstName:"Alex"}</code></td><td><code>.firstName,Alex,role,admin</code></td><td><code>.firstName=Alex.role=admin</code></td></tr> |
| 73 | + |
| 74 | +<tr><td rowspan="3"><code>matrix</code></td> |
| 75 | + <td>primitive <code>5</code></td><td><code>;id=5</code></td><td><code>;id=5</code></td></tr> |
| 76 | +<tr><td>array <code>[3,4,5]</code></td><td><code>;id=3,4,5</code></td><td><code>;id=3;id=4;id=5</code></td></tr> |
| 77 | +<tr><td>object <code>{role:"admin", firstName:"Alex"}</code></td><td><code>;id=firstName,Alex,role,admin</code></td><td><code>;firstName=Alex;role=admin</code></td></tr> |
| 78 | + |
| 79 | +<tr><td rowspan="3"><code>form</code></td> |
| 80 | + <td>primitive <code>5</code></td><td><code>id=5</code></td><td><code>id=5</code></td></tr> |
| 81 | +<tr><td>array <code>[3,4,5]</code></td><td><code>id=3,4,5</code></td><td><code>id=3&id=4&id=5</code></td></tr> |
| 82 | +<tr><td>object <code>{role:"admin", firstName:"Alex"}</code></td><td><code>id=firstName,Alex,role,admin</code></td><td><code>firstName=Alex&role=admin</code></td></tr> |
| 83 | + |
| 84 | +<tr><td rowspan="3"><code>spaceDelimited</code></td> |
| 85 | + <td>primitive</td><td colspan="2"><em>not supported</em></td></tr> |
| 86 | +<tr><td>array <code>[3,4,5]</code></td><td><code>id=3 4 5</code></td><td><code>id=3&id=4&id=5</code></td></tr> |
| 87 | +<tr><td>object</td><td colspan="2"><em>not supported</em></td></tr> |
| 88 | + |
| 89 | +<tr><td rowspan="3"><code>pipeDelimited</code></td> |
| 90 | + <td>primitive</td><td colspan="2"><em>not supported</em></td></tr> |
| 91 | +<tr><td>array <code>[3,4,5]</code></td><td><code>id=3|4|5</code></td><td><code>id=3&id=4&id=5</code></td></tr> |
| 92 | +<tr><td>object</td><td colspan="2"><em>not supported</em></td></tr> |
| 93 | + |
| 94 | +<tr><td rowspan="3"><code>deepObject</code></td> |
| 95 | + <td>primitive</td><td colspan="2"><em>not supported</em></td></tr> |
| 96 | +<tr><td>array <code>[3,4,5]</code></td><td colspan="2"><code>id[0]=3&id[1]=4&id[2]=5</code> (explode required)</td></tr> |
| 97 | +<tr><td>object <code>{role:"admin", firstName:"Alex"}</code></td><td colspan="2"><code>id[firstName]=Alex&id[role]=admin</code> (explode required)</td></tr> |
| 98 | +</tbody> |
| 99 | +</table> |
| 100 | + |
| 101 | +> The above outputs are shown unescaped for readability. In real use, values |
| 102 | +> destined for query parameters are passed through `url.QueryEscape` (or |
| 103 | +> `url.PathEscape` for path parameters), so reserved characters and |
| 104 | +> non-ASCII bytes are percent-encoded on the wire. |
| 105 | +
|
| 106 | +### Parameter Limitations |
| 107 | + |
| 108 | +The OpenAPI 3.x parameter styles are convenient but each has at least one |
| 109 | +sharp edge. The list below documents behaviors that surprise users and the |
| 110 | +cases where round-tripping is not possible in principle. |
| 111 | + |
| 112 | +#### Encoding |
| 113 | + |
| 114 | +- **Query and path values are percent-encoded.** Reserved characters |
| 115 | + (`&`, `=`, `#`, `?`, etc.) and non-ASCII bytes are escaped via |
| 116 | + `url.QueryEscape` / `url.PathEscape`. Spaces in query values are encoded |
| 117 | + as `+` (form-urlencoded convention), matching `url.Values.Encode()`. |
| 118 | +- **Header values are passed through raw.** Per RFC 7230 §3.2.6, header |
| 119 | + field values may contain visible ASCII plus space/tab; bytes ≥ `0x80` are |
| 120 | + `obs-text` and explicitly marked obsolete in RFC 9110. There is no |
| 121 | + generally-agreed mechanism for transporting non-ASCII text in arbitrary |
| 122 | + header values (RFC 8187 covers only header *parameters* like |
| 123 | + `Content-Disposition` `filename*=`). If your spec puts non-ASCII or |
| 124 | + control characters into a header parameter, the wire format is |
| 125 | + RFC-noncompliant and proxies may strip or reject the request. |
| 126 | +- **Cookie values are passed through raw.** Per RFC 6265 §4.1.1, cookie |
| 127 | + values may not contain `CTL`, whitespace, `"`, `,`, `;`, `\`, or any byte |
| 128 | + ≥ `0x80`. Most cookie libraries URL-encode by convention, but this |
| 129 | + package does not — if your spec puts spaces or non-ASCII into a cookie |
| 130 | + parameter, the value will not be RFC 6265-conformant. |
| 131 | +- **Map keys are percent-encoded for `deepObject` only.** For `simple`, |
| 132 | + `label`, `matrix`, and `form` styles, map keys are emitted raw. If your |
| 133 | + map keys are non-ASCII or contain URL-reserved characters, the wire |
| 134 | + representation will not be encoded. |
| 135 | +- **`allowReserved`** (`StyleParamOptions.AllowReserved`) is a query-only |
| 136 | + option per the OpenAPI 3.x spec, and only applies to *values*, not |
| 137 | + parameter names or map keys. |
| 138 | + |
| 139 | +#### `deepObject` |
| 140 | + |
| 141 | +- **Bracket notation is structural, not data.** `MarshalDeepObject` |
| 142 | + percent-encodes literal `[` and `]` inside values and map keys as `%5B` |
| 143 | + / `%5D` on the wire. However, once a server calls `url.ParseQuery`, those |
| 144 | + escapes are decoded back to `[` and `]` — at which point a key like |
| 145 | + `p[a[b]]` is ambiguous between `{p: {a: {b: ...}}}` and |
| 146 | + `{p: {"a[b]": ...}}`. `UnmarshalDeepObject` cannot distinguish these |
| 147 | + cases and adopts the same greedy left-to-right parse used by qs (Node), |
| 148 | + Rails `Rack::Utils.parse_nested_query`, and similar libraries: every |
| 149 | + unescaped `[` opens a new nesting level. **Literal `[` and `]` inside |
| 150 | + map keys cannot be round-tripped.** Use a different separator in |
| 151 | + user-supplied keys if this matters to you. |
| 152 | +- **OpenAPI 3.x defines `deepObject` only for object schemas.** This |
| 153 | + package extends it to maps and arrays for convenience (arrays are |
| 154 | + numerically indexed: `p[0]`, `p[1]`, …), but other tooling may not |
| 155 | + accept that wire form. |
| 156 | +- **`deepObject` requires `explode: true`.** Non-exploded `deepObject` has |
| 157 | + no well-defined wire format; an error is returned. |
| 158 | + |
| 159 | +#### `spaceDelimited` / `pipeDelimited` |
| 160 | + |
| 161 | +- **Array-only.** Per the OpenAPI spec, these styles only apply to arrays |
| 162 | + of primitives. Passing a primitive or object returns an error. |
| 163 | +- **Exploded form degenerates to `form`.** When `explode: true`, the |
| 164 | + separator becomes `&` (not space or pipe), so the output is |
| 165 | + byte-identical to `form` exploded. The space/pipe separator only takes |
| 166 | + effect when `explode: false`. This is per the OpenAPI spec, but it |
| 167 | + surprises many users. |
| 168 | +- **Unexploded `spaceDelimited` is RFC-fragile.** Literal spaces in a |
| 169 | + query string are tolerated by most parsers but are not RFC 3986- |
| 170 | + conformant; `+` would be the form-urlencoded canonical form for space, |
| 171 | + but `spaceDelimited` is defined to use literal `%20`-equivalent space |
| 172 | + (the value bytes themselves are then encoded normally). |
| 173 | + |
| 174 | +#### Type-conversion edge cases |
| 175 | + |
| 176 | +- **`null`** in a struct field marshals to the literal string `"null"` in |
| 177 | + `deepObject` output. There is no distinct OpenAPI representation for |
| 178 | + absent vs. JSON-null in query parameters. |
| 179 | +- **`time.Time` and `types.Date`** are formatted via RFC 3339 and |
| 180 | + `2006-01-02` respectively when used as primitives in any style. If you |
| 181 | + want a different format, wrap the field in a type that implements |
| 182 | + `encoding.TextMarshaler`. |
| 183 | +- **`types.UUID`** stringifies to the canonical hyphenated 36-character |
| 184 | + form; query/path location escaping is a no-op (UUIDs are in the |
| 185 | + unreserved set). |
| 186 | +- **`json.Marshaler` is consulted for structs**, then the result is |
| 187 | + re-decoded with `UseNumber()` and re-styled. Numbers therefore retain |
| 188 | + their original precision, but the round-trip through JSON means struct |
| 189 | + field tags are honored (not raw Go field names). |
| 190 | + |
0 commit comments