12 KiB
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)