Aller au contenu

Content

Vapor's content API allows you to easily encode / decode Codable structs to / from HTTP messages. JSON encoding is used by default with out-of-the-box support for URL-Encoded Form and Multipart. The API is also configurable, allowing for you to add, modify, or replace encoding strategies for certain HTTP content types.

Overview

To understand how Vapor's content API works, you should first understand a few basics about HTTP messages. Take a look at the following example request.

POST /greeting HTTP/1.1
content-type: application/json
content-length: 18

{"hello": "world"}

This request indicates that it contains JSON-encoded data using the content-type header and application/json media type. As promised, some JSON data follows after the headers in the body.

Content Struct

The first step to decoding this HTTP message is creating a Codable type that matches the expected structure.

struct Greeting: Content {
    var hello: String
}

Conforming the type to Content will automatically add conformance to Codable alongside additional utilities for working with the content API.

Once you have the content structure, you can decode it from the incoming request using req.content.

app.post("greeting") { req in 
    let greeting = try req.content.decode(Greeting.self)
    print(greeting.hello) // "world"
    return HTTPStatus.ok
}

The decode method uses the request's content type to find an appropriate decoder. If there is no decoder found, or the request does not contain the content type header, a 415 error will be thrown.

That means that this route automatically accepts all of the other supported content types, such as url-encoded form:

POST /greeting HTTP/1.1
content-type: application/x-www-form-urlencoded
content-length: 11

hello=world

In the case of file uploads, your content property must be of type Data

struct Profile: Content {
    var name: String
    var email: String
    var image: Data
}

Supported Media Types

Below are the media types the content API supports by default.

name header value media type
JSON application/json .json
Multipart multipart/form-data .formData
URL-Encoded Form application/x-www-form-urlencoded .urlEncodedForm
Plaintext text/plain .plainText
HTML text/html .html

Not all media types support all Codable features. For example, JSON does not support top-level fragments and Plaintext does not support nested data.

Query

Vapor's Content APIs support handling URL encoded data in the URL's query string.

Decoding

To understand how decoding a URL query string works, take a look at the following example request.

GET /hello?name=Vapor HTTP/1.1
content-length: 0

Just like the APIs for handling HTTP message body content, the first step for parsing URL query strings is to create a struct that matches the expected structure.

struct Hello: Content {
    var name: String?
}

Note that name is an optional String since URL query strings should always be optional. If you want to require a parameter, use a route parameter instead.

Now that you have a Content struct for this route's expected query string, you can decode it.

app.get("hello") { req -> String in 
    let hello = try req.query.decode(Hello.self)
    return "Hello, \(hello.name ?? "Anonymous")"
}

This route would result in the following response given the example request from above:

HTTP/1.1 200 OK
content-length: 12

Hello, Vapor

If the query string were omitted, like in the following request, the name "Anonymous" would be used instead.

GET /hello HTTP/1.1
content-length: 0

Single Value

In addition to decoding to a Content struct, Vapor also supports fetching single values from the query string using subscripts.

let name: String? = req.query["name"]

Hooks

Vapor will automatically call beforeEncode and afterDecode on a Content type. Default implementations are provided which do nothing, but you can use these methods to run custom logic.

// Runs after this Content is decoded. `mutating` is only required for structs, not classes.
mutating func afterDecode() throws {
    // Name may not be passed in, but if it is, then it can't be an empty string.
    self.name = self.name?.trimmingCharacters(in: .whitespacesAndNewlines)
    if let name = self.name, name.isEmpty {
        throw Abort(.badRequest, reason: "Name must not be empty.")
    }
}

// Runs before this Content is encoded. `mutating` is only required for structs, not classes.
mutating func beforeEncode() throws {
    // Have to *always* pass a name back, and it can't be an empty string.
    guard 
        let name = self.name?.trimmingCharacters(in: .whitespacesAndNewlines), 
        !name.isEmpty 
    else {
        throw Abort(.badRequest, reason: "Name must not be empty.")
    }
    self.name = name
}

Override Defaults

The default encoders and decoders used by Vapor's Content APIs can be configured.

Global

ContentConfiguration.global lets you change the encoders and decoders Vapor uses by default. This is useful for changing how your entire application parses and serializes data.

// create a new JSON encoder that uses unix-timestamp dates
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .secondsSince1970

// override the global encoder used for the `.json` media type
ContentConfiguration.global.use(encoder: encoder, for: .json)

Mutating ContentConfiguration is usually done in configure.swift.

One-Off

Calls to encoding and decoding methods like req.content.decode support passing in custom coders for one-off usages.

// create a new JSON decoder that uses unix-timestamp dates
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970

// decodes Hello struct using custom decoder
let hello = try req.content.decode(Hello.self, using: decoder)

Custom Coders

Applications and third-party packages can add support for media types that Vapor does not support by default by creating custom coders.

Content

Vapor specifies two protocols for coders capable of handling content in HTTP message bodies: ContentDecoder and ContentEncoder.

public protocol ContentEncoder {
    func encode<E>(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders) throws
        where E: Encodable
}

public protocol ContentDecoder {
    func decode<D>(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders) throws -> D
        where D: Decodable
}

Conforming to these protocols allows your custom coders to be registered to ContentConfiguration as specified above.

URL Query

Vapor specifies two protocols for coders capable of handling content in URL query strings: URLQueryDecoder and URLQueryEncoder.

public protocol URLQueryDecoder {
    func decode<D>(_ decodable: D.Type, from url: URI) throws -> D
        where D: Decodable
}

public protocol URLQueryEncoder {
    func encode<E>(_ encodable: E, to url: inout URI) throws
        where E: Encodable
}

Conforming to these protocols allows your custom coders to be registered to ContentConfiguration for handling URL query strings using the use(urlEncoder:) and use(urlDecoder:) methods.

Custom ResponseEncodable

Another approach involves implementing ResponseEncodable on your types. Consider this trivial HTML wrapper type:

struct HTML {
  let value: String
}

Then its ResponseEncodable implementation would look like this:

extension HTML: ResponseEncodable {
  public func encodeResponse(for request: Request) -> EventLoopFuture<Response> {
    var headers = HTTPHeaders()
    headers.add(name: .contentType, value: "text/html")
    return request.eventLoop.makeSucceededFuture(.init(
      status: .ok, headers: headers, body: .init(string: value)
    ))
  }
}

If you're using async/await you can use AsyncResponseEncodable:

extension HTML: AsyncResponseEncodable {
  public func encodeResponse(for request: Request) async throws -> Response {
    var headers = HTTPHeaders()
    headers.add(name: .contentType, value: "text/html")
    return .init(status: .ok, headers: headers, body: .init(string: value))
  }
}

Note that this allows customizing the Content-Type header. See HTTPHeaders reference for more details.

You can then use HTML as a response type in your routes:

app.get { _ in
  HTML(value: """
  <html>
    <body>
      <h1>Hello, World!</h1>
    </body>
  </html>
  """)
}