How to properly create a URL with parameters given by a custom type?

TL;DR - the API cannot be changed, and the variable parameters are a vital part of my app.

I've created a type that stores parameters for an API call.

To make use of them with a successful request, I first need to format them into the URL.

Say, my parameters are expressed as such:

struct Parameters: Encodable {
  var name: String
  var taste: Taste
  var numberOfSpoons: Int?
  var topping: Taste?

  private enum CodingKeys: String, CodingKey {
    case name, taste, topping
    case numberOfSpoons = "spoons"
  }
  
  init(name: String, taste: Taste, numberOfSpoons: Int? = 1, topping: Taste? = nil){
    self.name = name
    self.taste = taste
    self.numberOfSpoons = numberOfSpoons
    self.topping = topping
  }
}

Notice that my structure uses both Optionals (as the API does not need all the parameters from my app) and CodingKeys (as the names requested by the API look rather ugly in the code, but are necessary).

Here's the Taste - not much going on except explicit Encodable conformance.

enum Taste: String, Encodable {
  case choco, strawberry, vanilla
}

Now I want to call the API, so, according to the suggestion of John Sundell (I can't provide a link to the article due to Apple's censorship), I'm using URLComponents. For example:

var components = URLComponents()
components.scheme = "ftp"
components.host = "icecream.ogs"
components.path = "/spoons"

If I were to add queryItems by hand, it would be fairly easy.

components.queryItems = [
  URLQueryItem(name: "name", value: iceCreamParameters.name),
  URLQueryItem(name: "numberOfSpoons", value: "\(iceCreamParameters.numberOfSpoons!)")
]

How to dynamically create URLQueryItems from a non-CaseIterable type?

The URLQueryItem needs both name and value parameters. I want the i-th name to be equal to I-thParameters.PropertyName, the same goes for values.

As CaseIterable would not be feasible to implement* in the Parameters struct, I tried to fake it using the Encodable protocol - I thought about serializing the type to JSON (as the encoder gets rid of nil values, too) and then somehow get the keys from a deserialized Dictionary of AnyType (?), but there's got to be a better way.

If this approach is not feasible, please provide your answers with helpful examples.

Answered by DTS Engineer in 717751022

What you want here is a TopLevelEncoder, like JSONEncode, that encodes to a URLQueryItems. Building that is possible but challenging. I know folks who’ve done this for other serialisation formats but my experience is that it involves a lot of work dealing with all the corner cases.

I thought about serializing the type to JSON

That’s what I’d do if I were in your shoes. This is pretty straightforward, at least for the example you posted:

let p = Parameters(name: "99", taste: .vanilla, numberOfSpoons: 1, topping: .choco)
let jsonData = try JSONEncoder().encode(p)
let d = try JSONSerialization.jsonObject(with: jsonData) as! [String: Any]
let q = d.map { k, v in URLQueryItem(name: k, value: "\(v)") }
print(q)
// [spoons=1, taste=vanilla, name=99, topping=choco]

There are other ways you could approach this — driving this from an array of key paths, property wrappers, dynamic value lookup, and so on — but I think it’s going to be hard to beat the above.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

I can't provide a link to the article

Do you mean this article?

https://www.swiftbysundell.com/articles/constructing-urls-in-swift/

FYI, modern versions of DevForums will let you post any URL as long as you do it in the clear.

I’ll post a second reply regarding your specific question.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Accepted Answer

What you want here is a TopLevelEncoder, like JSONEncode, that encodes to a URLQueryItems. Building that is possible but challenging. I know folks who’ve done this for other serialisation formats but my experience is that it involves a lot of work dealing with all the corner cases.

I thought about serializing the type to JSON

That’s what I’d do if I were in your shoes. This is pretty straightforward, at least for the example you posted:

let p = Parameters(name: "99", taste: .vanilla, numberOfSpoons: 1, topping: .choco)
let jsonData = try JSONEncoder().encode(p)
let d = try JSONSerialization.jsonObject(with: jsonData) as! [String: Any]
let q = d.map { k, v in URLQueryItem(name: k, value: "\(v)") }
print(q)
// [spoons=1, taste=vanilla, name=99, topping=choco]

There are other ways you could approach this — driving this from an array of key paths, property wrappers, dynamic value lookup, and so on — but I think it’s going to be hard to beat the above.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

How to properly create a URL with parameters given by a custom type?
 
 
Q