-
Notifications
You must be signed in to change notification settings - Fork 135
Description
Motivation
One of the main benefits I get from using swift-openapi-generator is type safety for client code.
However, for multipart form upload requests, it seems like the property codegen is not typesafe, where all properties just get a generic HTTPBody
payload.
For example, given this OpenAPI document:
/uploadFile:
post:
operationId: uploadFile
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
description:
type: string
description: A text description of the uploaded file
count:
type: integer
description: An integer value associated with the upload
imageFile:
type: string
format: binary
description: The image file to upload
required:
- description
- count
- imageFile
This code is generated for the request body type:
enum Body: Sendable, Hashable {
enum MultipartFormPayload: Sendable, Hashable {
struct DescriptionPayload: Sendable, Hashable {
var body: OpenAPIRuntime.HTTPBody
init(body: OpenAPIRuntime.HTTPBody) {
self.body = body
}
}
case description(OpenAPIRuntime.MultipartPart<Operations.UploadFile.Input.Body.MultipartFormPayload.DescriptionPayload>)
struct CountPayload: Sendable, Hashable {
var body: OpenAPIRuntime.HTTPBody
init(body: OpenAPIRuntime.HTTPBody) {
self.body = body
}
}
case count(OpenAPIRuntime.MultipartPart<Operations.UploadFile.Input.Body.MultipartFormPayload.CountPayload>)
struct ImageFilePayload: Sendable, Hashable {
var body: OpenAPIRuntime.HTTPBody
init(body: OpenAPIRuntime.HTTPBody) {
self.body = body
}
}
case imageFile(OpenAPIRuntime.MultipartPart<Operations.UploadFile.Input.Body.MultipartFormPayload.ImageFilePayload>)
case undocumented(OpenAPIRuntime.MultipartRawPart)
}
case multipartForm(OpenAPIRuntime.MultipartBody<Operations.UploadFile.Input.Body.MultipartFormPayload>)
}
So at the call site, you're responsible for producing HTTPBody
values that respect the schema, but you get no compile-type safety if you make a mistake:
let multipartBody: MultipartBody<Operations.UploadFile.Input.Body.MultipartFormPayload> = [
.description(.init(payload: .init(body: "This is a test image upload"))),
.count(.init(payload: .init(body: "42"))),
.imageFile(.init(payload: .init(body: .init(fileData)))),
]
Ideally, the codegen would produce type safe property payload initializers:
let multipartBody: MultipartBody<Operations.UploadFile.Input.Body.MultipartFormPayload> = [
.description(.init(payload: .init(value: "This is a test image upload"))),
.count(.init(payload: .init(value: 42))),
.imageFile(.init(payload: .init(value: fileData))),
]
and you'd get a compiler failure if you passed say a Swift.String
as the count value.
Proposed solution
Ideally, the codegen would produce type safe property payload initializers:
let multipartBody: MultipartBody<Operations.UploadFile.Input.Body.MultipartFormPayload> = [
.description(.init(payload: .init(value: "This is a test image upload"))),
.count(.init(payload: .init(value: 42))),
.imageFile(.init(payload: .init(value: fileData))),
]
and you'd get a compiler failure if you passed say a Swift.String
as the count value.
Alternatives considered
No response
Additional information
No response
Activity
jpsim commentedon Apr 2, 2025
Also, let me know if I should open a new issue for this, but there's also a lack of type safety related to missing a required fields.
In my example above, if you forget to add the
count
property inmultipartBody
, the request will just hang until it times out, without ever making an http request. Might be nice to have an alternative where you produce aUploadFileRequestBody
struct with typesafe properties up front in an initializer. You wouldn't be able to use this in all cases, but in my case I know all the fields I want to populate up front before performing the request.And a third (final!) thing on multipart form upload ergonomics, in my case I want to specify a dynamic content type and file name for my
image
property, but instead of doing.image(custom overrides...)
I have to resort to.undocumented(MultipartRawPart(custom overrides...)
. Ideally I could just pass the filename and content type directly in the codegened.image(...)
enum.czechboy0 commentedon Apr 2, 2025
Hi @jpsim, thanks for the detailed issue(s) 🙂
On the first one, a multipart part of type "string" will always be HTTPBody (an async sequence of chunks), because we don't know if you want to buffer it or not, so we give you the stream always. That's meant for things like multi-GB log files (which could also be a part of type string).
Now, the integer at the top level being HTTPBody is unexpected to me, and we'll need to look into it.
More generally, I think you'll get the best results if your multipart parts are almost like request bodies - each of a different content type (or multiple files). So if I were authoring your example, I'd write it as:
Although I appreciate that this might be an upstream service's OpenAPI that you might not be able to change.
jpsim commentedon Apr 2, 2025
I appreciate the benefits of having a performant and streaming friendly API, however for small strings it'd be nice to have a typesafe initializer.
Same thing goes for string-backed enums!
czechboy0 commentedon Apr 2, 2025
I agree - we just don't know which strings will be larger or small. The OpenAPI document doesn't include that information.
jpsim commentedon Apr 2, 2025
That's why I think it should be up to the caller to determine which to use.
For example, for a string
description
property:czechboy0 commentedon Apr 3, 2025
I see, so is this the main ask? For the generated multipart part to go from e.g.
to
I think that's reasonable, and is an API-stable change.
jpsim commentedon Apr 3, 2025
Yes that's the main ask, to have typesafe initializers for each property payload type in addition to the current generic
HTTPBody
initializer. Not just for strings, but for all types supported by swift-openapi-generator. Integers, booleans, enums, data, dates, etc.Optionally deprecating the
body
initializer in favor of a renameduncheckedBody
initializer would also encourage the use of the type safe initializer in most cases, and consumers can still use it for streaming use cases, but there's no schema validation if you do that. Marking an API as deprecated is usually considered to be an API-stable change. But I don't feel strongly about this one.Add convenience initializers for primitive-typed multipart parts
jpsim commentedon May 27, 2025
Proposed implementation (only for primitive types though): #775