Skip to content

Add convenience initializers for primitive-typed multipart parts #775

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

jpsim
Copy link

@jpsim jpsim commented May 27, 2025

Generate additional init(value:) initializers for multipart payload structs when the part is defined with primitive types (integer, number, string, or boolean) in the OpenAPI document. This provides a more ergonomic, typesafe API by allowing direct value initialization instead of requiring manual HTTPBody construction with no type validation.

The generated initializers automatically convert numeric and boolean values to strings as required by multipart form encoding.

Before/After Examples:

Type Before After
integer init(body: HTTPBody("123")) init(value: 123)
number init(body: HTTPBody("45.67")) init(value: 45.67)
string init(body: HTTPBody("hello")) init(value: "hello")
boolean init(body: HTTPBody("true")) init(value: true)

The new initializers:

  • For integers: init(value: Int) - converts to string internally
  • For numbers: init(value: Double) - converts to string internally
  • For strings: init(value: String) - passes through directly
  • For booleans: init(value: Bool) - converts to string internally

These convenience initializers are also generated for multipart parts with custom headers. When headers are present, the generated initializer includes both headers and value parameters:

  • Without headers: init(value: String)
  • With headers: init(headers: Headers, value: String)

This partly resolves #756. Support for non-trivial types (objects, arrays, etc.) is not included in this change as it would require more complex type handling that I couldn't figure out.

jpsim added 2 commits May 27, 2025 09:58
Generate additional `init(value:)` initializers for multipart payload
structs when the part is defined with primitive types (integer, number,
string, or boolean) in the OpenAPI document. This provides a more
ergonomic, typesafe API by allowing direct value initialization instead
of requiring manual `HTTPBody` construction with no type validation.

The generated initializers automatically convert numeric and boolean
values to strings as required by multipart form encoding.

Before/After Examples:

| Type    | Before                          | After                  |
|---------|---------------------------------|------------------------|
| integer | `init(body: HTTPBody("123"))`   | `init(value: 123)`     |
| number  | `init(body: HTTPBody("45.67"))` | `init(value: 45.67)`   |
| string  | `init(body: HTTPBody("hello"))` | `init(value: "hello")` |
| boolean | `init(body: HTTPBody("true"))`  | `init(value: true)`    |

The new initializers:
- For integers: `init(value: Int)` - converts to string internally
- For numbers: `init(value: Double)` - converts to string internally
- For strings: `init(value: String)` - passes through directly
- For booleans: `init(value: Bool)` - converts to string internally

These convenience initializers are also generated for multipart parts
with custom headers. When headers are present, the generated initializer
includes both headers and value parameters:
- Without headers: `init(value: String)`
- With headers: `init(headers: Headers, value: String)`

This partly resolves
apple#756. Support
for non-trivial types (objects, arrays, etc.) is not included in this
change as it would require more complex type handling that I couldn't
figure out.
@czechboy0
Copy link
Contributor

Hi @jpsim,

thanks for the PR! For the conversion from primitive types (String, Int, Double, Bool), what's the serialization into bytes being defined by? The OpenAPI specification defines that for primitive types at the top level of the multipart schema are serialized as text/plain, but it doesn't define how to convert to/from primitive types and text/plain. While we could use the built-in Swift conventions, it'd be better if we could find how to do it in a more language-agnostic manner.

In addition, how would someone get the values back out in a similar way? Generally we've tried to have all APIs be symmetric - so if we generate serialization for something, we also generate deserialization. So we'd need a related specification that can convert from text/plain into primitive types.

Content types like application/json and application/x-www-form-urlencoded have that well-defined serialization/deserialization, but text/plain doesn't, because it's not structured. So I'm not really sure how to write a proper serializer/deserializer in a way that interops with OpenAPI-generated code in other languages.

@jpsim
Copy link
Author

jpsim commented May 27, 2025

The OpenAPI specification defines that for primitive types at the top level of the multipart schema are serialized as text/plain, but it doesn't define how to convert to/from primitive types and text/plain.

Ah TIL, this is really unfortunate as it appears to mean that #756 is fundamentally impossible! I did not realize that from our discussion in that issue.

It happens that the Swift String() initializers that take Double, Int and Bool will all produce valid JSON scalar values, so I think it could still be a reasonable default.

In addition, how would someone get the values back out in a similar way?

Since these are all just convenience initializers, someone could get the values back exactly the same way as they do today. Right now there's no default serialization convention on the sender's side or on the receiver's side. This change introduces a default serialization convention on the sender's side only. A sender can decide if they want to send a single scalar value, that's a safe assumption to make because they're the ones making the request, but a receiver (server) can't safely make the same assumption.


Ultimately I understand if you choose to reject this direction because it's making assumptions about the serialization format that aren't enforced by the OpenAPI schema. What if instead, we made this clear in the initializer that it assumes JSON scalar serialization by renaming the parameter from value to jsonValue?

@czechboy0
Copy link
Contributor

Ah TIL, this is really unfortunate as it appears to mean that #756 is fundamentally impossible! I did not realize that from our discussion in that issue.

Yeah it also only dawned on me that we don't have a spec/RFC to follow here.

What if instead, we made this clear in the initializer that it assumes JSON scalar serialization by renaming the parameter from value to jsonValue?

Serializing it as JSON would e.g. mean quoting strings, aka converting them to JSON fragments using JSONEncoder.

Although I'm worried about creating a side channel of assuming both sides are Swift. We've historically taken the opposite direction, and tried to stick to the spec as much as possible. I'll let @simonjbeaumont chime in as well.

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

Successfully merging this pull request may close these issues.

Multipart form properties codegen is not typesafe
2 participants