Skip to content

Using Content

In Vapor 3, all content types (JSON, protobuf, URLEncodedForm, Multipart, etc) are treated the same. All you need to parse and serialize content is a Codable class or struct.

For this introduction, we will use mostly JSON as an example. But keep in mind the API is the same for any supported content type.

Server

This first section will go over decoding and encoding messages sent between your server and connected clients. See the client section for encoding and decoding content in messages sent to external APIs.

Request

Let's take a look at how you would parse the following HTTP request sent to your server.

POST /login HTTP/1.1
Content-Type: application/json

{
    "email": "user@vapor.codes",
    "password": "don't look!"
}

First, create a struct or class that represents the data you expect.

import Vapor

struct LoginRequest: Content {
    var email: String
    var password: String
}

Notice the key names exactly match the keys in the request data. The expected data types also match. Next conform this struct or class to Content.

Decode

Now we are ready to decode that HTTP request. Every Request has a ContentContainer that we can use to decode content from the message's body.

router.post("login") { req -> Future<HTTPStatus> in
    return req.content.decode(LoginRequest.self).map { loginRequest in
        print(loginRequest.email) // user@vapor.codes
        print(loginRequest.password) // don't look!
        return HTTPStatus.ok
    }
}

We use .map(to:) here since decode(...) returns a future.

Note

Decoding content from requests is asynchronous because HTTP allows bodies to be split into multiple parts using chunked transfer encoding.

Router

To help make decoding content from incoming requests easier, Vapor offers a few extensions on Router to do this automatically.

router.post(LoginRequest.self, at: "login") { req, loginRequest in
    print(loginRequest.email) // user@vapor.codes
    print(loginRequest.password) // don't look!
    return HTTPStatus.ok
}

Detect Type

Since the HTTP request in this example declared JSON as its content type, Vapor knows to use a JSON decoder automatically. This same method would work just as well for the following request.

POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded

email=user@vapor.codes&don't+look!

All HTTP requests must include a content type to be valid. Because of this, Vapor will automatically choose an appropriate decoder or error if it encounters an unknown media type.

Tip

You can configure the default encoders and decoders Vapor uses.

Custom

You can always override Vapor's default decoder and pass in a custom one if you want.

let user = try req.content.decode(User.self, using: JSONDecoder())
print(user) // Future<User>

Response

Let's take a look at how you would create the following HTTP response from your server.

HTTP/1.1 200 OK
Content-Type: application/json

{
    "name": "Vapor User",
    "email": "user@vapor.codes"
}

Just like decoding, first create a struct or class that represents the data that you are expecting.

import Vapor

struct User: Content {
    var name: String
    var email: String
}

Then just conform this struct or class to Content.

Encode

Now we are ready to encode that HTTP response.

router.get("user") { req -> User in
    return User(name: "Vapor User", email: "user@vapor.codes")
}

This will create a default Response with 200 OK status code and minimal headers. You can customize the response using a convenience encode(...) method.

router.get("user") { req -> Future<Response> in
    return User(name: "Vapor User", email: "user@vapor.codes")
        .encode(status: .created)
}

Override Type

Content will automatically encode as JSON by default. You can always override which content type is used using the as: parameter.

try res.content.encode(user, as: .urlEncodedForm)

You can also change the default media type for any class or struct.

struct User: Content {
    /// See `Content`.
    static let defaultContentType: MediaType = .urlEncodedForm

    ...
}

Client

Encoding content to HTTP requests sent by Clients is similar to encoding HTTP responses returned by your server.

Request

Let's take a look at how we can encode the following request.

POST /login HTTP/1.1
Host: api.vapor.codes
Content-Type: application/json

{
    "email": "user@vapor.codes",
    "password": "don't look!"
}

Encode

First, create a struct or class that represents the data you expect.

import Vapor

struct LoginRequest: Content {
    var email: String
    var password: String
}

Now we are ready to make our request. Let's assume we are making this request inside of a route closure, so we will use the incoming request as our container.

let loginRequest = LoginRequest(email: "user@vapor.codes", password: "don't look!")
let res = try req.client().post("https://api.vapor.codes/login") { loginReq in
    // encode the loginRequest before sending
    try loginReq.content.encode(loginRequest)
}
print(res) // Future<Response>

Response

Continuing from our example in the encode section, let's see how we would decode content from the client's response.

HTTP/1.1 200 OK
Content-Type: application/json

{
    "name": "Vapor User",
    "email": "user@vapor.codes"
}

First of course we must create a struct or class to represent the data.

import Vapor

struct User: Content {
    var name: String
    var email: String
}

Decode

Now we are ready to decode the client response.

let res: Future<Response> // from the Client

let user = res.flatMap { try $0.content.decode(User.self) }
print(user) // Future<User>

Example

Let's now take a look at our complete Client request that both encodes and decodes content.

// Create the LoginRequest data
let loginRequest = LoginRequest(email: "user@vapor.codes", password: "don't look!")
// POST /login
let user = try req.client().post("https://api.vapor.codes/login") { loginReq in 
    // Encode Content before Request is sent
    return try loginReq.content.encode(loginRequest) 
}.flatMap { loginRes in
    // Decode Content after Response is received
    return try loginRes.content.decode(User.self) 
}
print(user) // Future<User>

Query String

URL-Encoded Form data can be encoded and decoded from an HTTP request's URI query string just like content. All you need is a class or struct that conforms to Content. In these examples, we will be using the following struct.

struct Flags: Content {
     var search: String?
     var isAdmin: Bool?
}

Decode

All Requests have a QueryContainer that you can use to decode the query string.

let flags = try req.query.decode(Flags.self)
print(flags) // Flags

Encode

You can also encode content. This is useful for encoding query strings when using Client.

let flags: Flags ...
try req.query.encode(flags)

Dynamic Properties

One of the most frequently asked questions regarding Content is:

How do I add a property to just this response?

The way Vapor 3 handles Content is based entirely on Codable. At no point (that is publically accessible) is your data in an arbitrary data structure like [String: Any] that you can mutate at will. Because of this, all data structures that your app accepts and returns must be statically defined.

Let's take a look at a common scenario to better understand this. Very often when you are creating a user, there are a couple different data formats required:

  • create: password should be supplied twice to check values match
  • internal: you should store a hash not the plaintext password
  • public: when listing users, the password hash should not be included

To do this, you should create three types.

// Data required to create a user
struct UserCreate: Content {
    var email: String
    var password: String
    var passwordCheck: String
}

// Our internal User representation
struct User: Model {
    var id: Int?
    var email: String
    var passwordHash: Data
}

// Public user representation
struct PublicUser: Content {
    var id: Int
    var email: String
}

// Create a router for POST /users
router.post(UserCreate.self, at: "users") { req, userCreate -> PublicUser in
    guard userCreate.password == passwordCheck else { /* some error */ }
    let hasher = try req.make(/* some hasher */)
    let user = try User(
        email: userCreate.email, 
        passwordHash: hasher.hash(userCreate.password)
    )
    // save user
    return try PublicUser(id: user.requireID(), email: user.email)
}

For other methods such as PATCH and PUT, you may want to create additional types to supports the unique semantics.

Benefits

This method may seem a bit verbose at first when compared to dynamic solutions, but it has a number of key advantages:

  • Statically Typed: Very little validation is needed on top of what Swift and Codable do automatically.
  • Readability: No need for Strings and optional chaining when working with Swift types.
  • Maintainable: Large projects will appreciate having this information separated and clearly stated.
  • Shareable: Types defining what content your routes accept and return can be used to conform to specifications like OpenAPI or even be shared directly with clients written in Swift.
  • Performance: Working with native Swift types is much more performant than mutating [String: Any] dictionaries.

JSON

JSON is a very popular encoding format for APIs and the way in which dates, data, floats, etc are encoded is non-standard. Because of this, Vapor makes it easy to use custom JSONDecoders when you interact with other APIs.

// Conforms to Encodable
let user: User ... 
// Encode JSON using custom date encoding strategy
try req.content.encode(json: user, using: .custom(dates: .millisecondsSince1970))

You can also use this method for decoding.

// Decode JSON using custom date encoding strategy
let user = try req.content.decode(json: User.self, using: .custom(dates: .millisecondsSince1970))

If you would like to set a custom JSON encoder or decoder globally, you can do so using configuration.

Configure

Use ContentConfig to register custom encoder/decoders for your application. These custom coders will be used anywhere you do content.encode/content.decode.

/// Create default content config
var contentConfig = ContentConfig.default()

/// Create custom JSON encoder
var jsonEncoder = JSONEncoder()
jsonEncoder.dateEncodingStrategy = .millisecondsSince1970

/// Register JSON encoder and content config
contentConfig.use(encoder: jsonEncoder, for: .json)
services.register(contentConfig)