Skip to main content
Version: 2.x

Form Data

The Form represents a collection of FormFields that can be a multipart or URL-encoded form:

final case class Form(formData: Chunk[FormField])

A Form is commonly used in request bodies for handling data from HTML forms and file uploads, although it can also be utilized in response bodies.

Form Fields

A FormField is a field within a Form and consists of a name, content type, type-specific content, and an optional filename.

sealed trait FormField {
def name: String
def contentType: MediaType
def filename: Option[String]
}

There are four types of FormField: Simple FormField, Text FormField, Binary FormField, and StreamingBinary FormField.

Simple FormField

Simple form fields are represented by the Simple case class. They consist of a simple key-value pair containing a name and a value (String). Unlike Binary and Text, they do not contain additional metadata such as content type or filename:

final case class Simple(name: String, value: String) extends FormField {
override val contentType: MediaType = MediaType.text.plain
override val filename: Option[String] = None
}

To create a simple form field, we can use FormField.simpleField constructor:

import zio.http._

val simpleFormField = FormField.simpleField("name", "value")

Instances of FormField.Simple are commonly used for transmitting simple textual data where additional metadata is not required, such as form fields in HTML forms.

Text FormField

Text form fields are represented by the Text case class. They contain textual data (String) along with metadata such as the content type and optionally the filename:

final case class Text(
name: String,
value: String,
contentType: MediaType,
filename: Option[String] = None,
) extends FormField

To create a text form field, we can use FormField.textField constructor:

import zio.http._

val textFormField1 = FormField.textField("name", "value")

val textFormField2 = FormField.textField("name", "value", MediaType.text.plain)

Instances of FormField.Text are used for transmitting simple textual data and textual files, such as text files, HTML files, and so on.

Binary FormField

Binary form fields are represented by the FormField.Binary case class. They contain binary data (Chunk[Byte]), along with metadata such as the content type (MediaType) and optionally the Content-Transfer-Encoding header field and filename:

final case class Binary(
name: String,
data: Chunk[Byte],
contentType: MediaType,
transferEncoding: Option[ContentTransferEncoding] = None,
filename: Option[String] = None,
) extends FormField

To create a binary form field, we can use FormField.binaryField constructor:

import zio._
import zio.http._
import zio.http.Header._

val image = Chunk.fromArray(???)

val binaryFormField = FormField.binaryField(
name = "profile pic",
data = image,
mediaType = MediaType.image.jpeg,
transferEncoding = Some(ContentTransferEncoding.Binary),
filename = Some("profile.jpg")
)
note

The data is not encoded in any way relative to the provided transferEncoding. It is the responsibility of the user to encode the data accordingly.

This form field is suitable for transmitting files or other binary data through HTTP requests.

The data is typically encoded in a way that can be transmitted as text (e.g., Base64 encoding) and decoded on the receiving end. The transfer encoding can be one of the following: SevenBit, EightBit, Binary, QuotedPrintable, Base64, and XToken.

Creating a Form

The Form's companion object offers several convenient methods for constructing form data, whether from individual form fields, key-value pairs, multipart bytes, query parameters, or URL-encoded data. We'll cover each of these methods and provide examples to illustrate their usage.

Creating an Empty Form

We can create an empty form using the empty method:

import zio.http._

val emptyForm = Form.empty

This creates an empty form with no fields.

Creating a Form from Form Fields

We can create a form by providing individual form fields using the Form.apply method. This method takes one or more FormField objects:

import zio.http._

val form = Form(
FormField.Simple("name", "John"),
FormField.Simple("age", "42"),
)

Creating a Form from Key-Value Pairs

We can create a form from key-value pairs using the Form.fromStrings method:

import zio.http._

val formData = Form.fromStrings(
"username" -> "johndoe",
"password" -> "secret",
)

Decoding Raw Multipart Bytes into a Form

We can create a form from multipart bytes using the Form.fromMultipartBytes method. This is useful when handling multipart form data received in HTTP requests.

Assume we have received the following multipart data:

import zio.http._

val multipartBytes =
s"""|--boundary123\r
|Content-Disposition: form-data; name="field1"\r
|\r
|value1\r
|--boundary123\r
|Content-Disposition: form-data; name="field2"\r
|\r
|value2\r
|--boundary123\r
|Content-Disposition: form-data; name="file1"; filename="filename1.txt"\r
|Content-Type: text/plain\r
|\r
|Contents of filename1.txt\r
|--boundary123\r
|Content-Disposition: form-data; name="file2"; filename="filename2.txt"\r
|Content-Type: text/plain\r
|\r
|Contents of filename2.txt\r
|--boundary123--\r\n""".stripMargin.getBytes(Charsets.Utf8)

We can decode it with the following code:

import zio._
import zio.http._

val charset = Charsets.Utf8

val formTask: Task[Form] =
Form.fromMultipartBytes(Chunk.fromArray(multipartBytes), charset, Some(Boundary("boundary123")))

val formData: Task[Chunk[FormField]] = formTask.map(_.formData)

Creating a Form from Query Parameters

We can create a form from query parameters using the Form.fromQueryParams method:

import zio.http._

val queryParams: QueryParams = QueryParams(
"name" -> "John",
"age" -> "42"
)

val form = Form.fromQueryParams(queryParams)

Creating a Form from URL-Encoded Data

We can create a form from URL-encoded data using the Form.fromURLEncoded method:

import zio.http._

val encodedData: String = "username=johndoe&password=secret"
// encodedData: String = "username=johndoe&password=secret"

val formResult: Either[FormDecodingError, Form] =
Form.fromURLEncoded(encodedData, Charsets.Utf8)
// formResult: Either[FormDecodingError, Form] = Right(
// value = Form(
// formData = IndexedSeq(
// Simple(name = "username", value = "johndoe"),
// Simple(name = "password", value = "secret")
// )
// )
// )

Operations

Appending Fields to a Form

We can append fields to an existing form using the + or append operator:

import zio.http._

val form =
Form(
FormField.simpleField("username", "johndoe"),
FormField.simpleField("password", "secretpassword"),
) + FormField.simpleField("age", "42")
// form: Form = Form(
// formData = IndexedSeq(
// Simple(name = "username", value = "johndoe"),
// Simple(name = "password", value = "secretpassword"),
// Simple(name = "age", value = "42")
// )
// )

Accessing Fields in a Form

We can access fields in a form using the get method, which returns an option containing the first field with the specified name:

form.get("username")
// res8: Option[FormField] = Some(
// value = Simple(name = "username", value = "johndoe")
// )

This method allows us to retrieve a specific field from the form by its name.

Encoding Forms

We can encode forms using multipart encoding or URL encoding.

Multipart Encoding

The Form#multipartBytes method takes the boundary and encodes the form using multipart encoding and returns the multipart byte stream:

import zio.http._
import zio.stream._

val form: Form = ???
val multipartStream: ZStream[Any, Nothing, Byte] =
form.multipartBytes(Boundary("boundary123"))

URL Encoding

The Form#urlEncoded method encodes the form using URL encoding and returns the encoded string:

import zio.http._

form.urlEncoded
// res10: String = "username=johndoe&password=secretpassword&age=42"

form.urlEncoded(Charsets.Utf8)
// res11: String = "username=johndoe&password=secretpassword&age=42"

Converting Form to Query Parameters

The toQueryParams method in the Form object allows us to convert a form into query parameters:

import zio.http._

val queryParams: QueryParams = form.toQueryParams
// queryParams: QueryParams = JavaLinkedHashMapQueryParams(
// underlying = {username=[johndoe], password=[secretpassword], age=[42]}
// )

queryParams.encode
// res12: String = "?username=johndoe&password=secretpassword&age=42"