apple / swift-openapi-generator

Generate Swift client and server code from an OpenAPI document.
https://swiftpackageindex.com/apple/swift-openapi-generator/documentation
Apache License 2.0
1.21k stars 87 forks source link

Convert the repsonse to an array of structs #541

Closed JPM-Tech closed 2 months ago

JPM-Tech commented 2 months ago

Question

I am trying to convert the response into an array of swift objects, but I'm having a hard time figuring out how this should work?

Below is my example code:

// object I want to use
struct Post: Identifiable, Codable {
    let userId: Int
    let id: Int
    let title: String
    let body: String
}

struct ContentView: View {
  @State var postData = [Post]()

  var body: some View {
      VStack {
          List(postData) { post in
              Text(post.title)
          }
          Button("Get Data") {
              Task {
                  try? await getData()
              }
          }
      }
  }

  func getData() async throws {
      let response = try await client.getPosts(Operations.getPosts.Input())

      switch response {
      case .ok(let okResponse):
          switch okResponse.body {
          case .json(let postResponse):
              // this doesn't work
              postData = postResponse
          }
      case .undocumented(statusCode: let statusCode, _):
          print("Undocumented Error: \(statusCode)")
      }
  }
}

When trying to assign the postResponse to the postData object gives the following error: Cannot assign value of type 'Components.Schemas.Post' to type '[Post]'

There aren't any examples that I could find of how to set an example list this up, and maybe there is something wrong with my openapi.yaml file (it was manually created since I don't own the service I am testing this API with)

openapi: "3.0.3"
info:
  title: "PostService"
  version: "1.0.0"
servers:
  - url: "https://mysamplepostresponse.com"
    description: "An endpoint to get various objects"
paths:
  /posts:
    get:
      operationId: "getPosts"
      responses:
        "200":
          description: "Returns an array of post objects"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Post"
components:
  schemas:
    Post:
      required:
        - userId
        - id
        - title
        - body
      type: object
      properties:
        userId:
          type: integer
          format: int64
          example: 1
        id:
          type: integer
          format: int64
          example: 1
        title:
          type: string
          example: "title ipsum"
        body:
          type: string
          example: "body ipsum"

Any help on how to do this the right way would be greatly appreciated

simonjbeaumont commented 2 months ago

Hey @JPM-Tech, thanks for reaching out.

There are at least two problems here:

  1. Your OpenAPI document for the getPosts operation doesn't return an array, but a single JSON object. You'll need to specify in the OpenAPI document that this returns an array.

  2. You'll need to either use the generated type (Components.Schemas.Post) as your array element type or otherwise provide a mapping to the manually defined Post type you created. The latter shouldn't be necessary as the whole point of generating these types is that you don't have to handwrite them.

JPM-Tech commented 2 months ago

That gives me a direction to go. Thank you @simonjbeaumont for your help! Would you mind sharing an example of what the suggested code might look like, and how it might be used in the view code above?

simonjbeaumont commented 2 months ago

@JPM-Tech I’ll take a look at this tomorrow, sure.

JPM-Tech commented 2 months ago

That would be awesome! If it helps, this is a sample of the JSON that I am getting back from the service:

[
  {
    "userId": 1,
    "id": 1,
    "title": "sunt aut facere",
    "body": "quia et suscipit\n"
  },
  {
    "userId": 1,
    "id": 2,
    "title": "qui est esse",
    "body": "est rerum tempore\n"
  }
]
JPM-Tech commented 2 months ago

I was able to continue working and came up with a working solution. For anyone else who comes across this in the future. My openapi.yaml was slightly off, here is the corrected version:

openapi: "3.0.3"
info:
  title: "PostService"
  version: "1.0.0"
servers:
  - url: "https://jsonplaceholder.typicode.com"
    description: "An endpoint to get various objects"
paths:
  /posts:
    get:
      operationId: "getPosts"
      responses:
        "200":
          description: "Returns an array of post objects"
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Post"
components:
  schemas:
    Post:
      type: object
      required:
        - userId
        - id
        - title
        - body
      properties:
        userId:
          type: integer
          example: 1
        id:
          type: integer
          example: 1
        title:
          type: string
          example: "title ipsum"
        body:
          type: string
          example: "body ipsum"

The view code now looks like the following:

    @State var postData = [Components.Schemas.Post]()

    func getData() async throws {
        let response = try await client.getPosts(Operations.getPosts.Input())

        switch response {
        case .ok(let okResponse):
            switch okResponse.body {
            case .json(let postResponse):
                postData = postResponse
            }
        case .undocumented(statusCode: let statusCode, _):
            print("Undocumented Error: \(statusCode)")
        }
    }

    var body: some View {
        VStack {
            List(postData, id: \.id) { post in
                Text(post.title)
            }
            Button("Get Data") {
                Task {
                    try? await getData()
                }
            }
        }
    }
czechboy0 commented 2 months ago

That looks good @JPM-Tech - anything else we can help with? Otherwise, please close this issue 🙂

simonjbeaumont commented 2 months ago

@JPM-Tech that looks exactly what I meant by both (1) and (2) in my above message. Glad you got it working.

There's an additional shorthand you can use if you don't want to switch over all the responses and content types, for which there are only one of each here. It's less defensive, but if you're only interested in the outcome you expect and otherwise are happy to swallow the error, you can use the following:

     @State var postData = [Components.Schemas.Post]()

     func getData() async throws {
-        let response = try await client.getPosts(Operations.getPosts.Input())
-        
-         switch response {
-         case .ok(let okResponse):
-             switch okResponse.body {
-             case .json(let postResponse):
-                 postData = postResponse
-             }
-         case .undocumented(statusCode: let statusCode, _):
-             print("Undocumented Error: \(statusCode)")
-         }
+       postData = try await client.getPosts().ok.json
+       //                                  ^  ^
+       //                                  |  ` .ok and .json are throwing computed properties.
+       //                                  `- No request arguments, so no need to provide input.
    }
JPM-Tech commented 2 months ago

Awesome work! Thank you!