| id | title |
|---|---|
media-type |
MediaType |
MediaType is a type-safe representation of IANA media types (also known as MIME types). It captures structured metadata about content types including compressibility, binary/text classification, and associated file extensions.
ZIO Blocks MediaType is designed to be a comprehensive and efficient implementation for handling media types in Scala applications, especially those involving HTTP content negotiation, file handling, and data serialization.
MediaType:
- is a zero-dependency data type in the
zio-blocks-mediatypemodule - ships with 2,600+ predefined IANA media types auto-generated from the mime-db database
- provides a
mediaType"..."string interpolator with compile-time validation - supports wildcard and case-insensitive matching
- is cross-platform (JVM and Scala.js) and cross-version (Scala 2.13 and 3.x)
final case class MediaType(
mainType: String,
subType: String,
compressible: Boolean = false,
binary: Boolean = false,
fileExtensions: List[String] = Nil,
extensions: Map[String, String] = Map.empty,
parameters: Map[String, String] = Map.empty
)Media types are fundamental to content negotiation in HTTP, file type detection, and serialization format selection. Working with raw strings like "application/json" is error-prone — typos go undetected, metadata (is it compressible? binary? what file extensions?) must be tracked separately, and matching logic must account for wildcards and case insensitivity.
MediaType solves these problems by providing:
- Structured representation — main type, subtype, and parameters as distinct fields
- Rich metadata — compressibility, binary/text classification, and file extensions baked in
- Compile-time safety — the
mediaType"..."interpolator catches malformed types at compile time - Correct matching — wildcard and case-insensitive matching with parameter awareness
MediaType
┌──────────┼──────────┐
mainType subType parameters
│ │ │
"application" "json" Map("charset" -> "utf-8")
│
┌─────┴──────────────────────────────────┐
│ compressible = true │
│ binary = false │
│ fileExtensions = List("json", "map") │
└────────────────────────────────────────┘
A quick example:
import zio.blocks.mediatype._
// Compile-time validated media type
val json = mediaType"application/json"
// Look up by file extension
val detected = MediaType.forFileExtension("png")
// Some(MediaType("image", "png", binary = true, ...))
// Wildcard matching
val textAny = mediaType"text/*"
val html = mediaType"text/html"
textAny.matches(html) // trueAdd the following to your build.sbt:
libraryDependencies += "dev.zio" %% "zio-blocks-mediatype" % "@VERSION@"For cross-platform projects (Scala.js):
libraryDependencies += "dev.zio" %%% "zio-blocks-mediatype" % "@VERSION@"Supported Scala versions: 2.13.x and 3.x.
We can create a MediaType by specifying the main type and subtype directly. All other fields have sensible defaults:
import zio.blocks.mediatype.MediaType
// Minimal — just mainType and subType
val plain = MediaType("text", "plain")
// compressible = false, binary = false, fileExtensions = Nil, ...
// With all fields
val json = MediaType(
mainType = "application",
subType = "json",
compressible = true,
binary = false,
fileExtensions = List("json", "map"),
extensions = Map("source" -> "iana"),
parameters = Map("charset" -> "utf-8")
)MediaType.parse parses a standard media type string in the format mainType/subType[; key=value]*:
import zio.blocks.mediatype.{MediaType, MediaTypes}
// Simple type
val json: Either[String, MediaType] = MediaType.parse("application/json")
// Right(MediaType("application", "json", compressible = true, ...))
// With parameters
val html = MediaType.parse("text/html; charset=utf-8")
// Right(MediaType("text", "html", ..., parameters = Map("charset" -> "utf-8")))
// Predefined instances are reused — parse returns the same object
val parsed = MediaType.parse("application/json").toOption.get
parsed eq MediaTypes.application.`json` // true (reference equality)
// Invalid input returns Left with an error message
MediaType.parse("") // Left("Invalid media type: cannot be empty")
MediaType.parse("applicationjson") // Left("Invalid media type: must contain '/' separator")
MediaType.parse("/json") // Left("Invalid media type: main type cannot be empty")
MediaType.parse("application/") // Left("Invalid media type: subtype cannot be empty")When we are certain the input is valid, unsafeFromString returns a MediaType directly or throws an IllegalArgumentException:
import zio.blocks.mediatype.MediaType
val json = MediaType.unsafeFromString("application/json")
// MediaType("application", "json", compressible = true, ...)
// Throws IllegalArgumentException for invalid input:
// MediaType.unsafeFromString("invalid")The mediaType"..." interpolator validates the media type at compile time. Invalid types produce compile errors, not runtime failures:
import zio.blocks.mediatype._
val json = mediaType"application/json"
val htmlUtf8 = mediaType"text/html; charset=utf-8"
val wildcard = mediaType"*/*"
val vendor = mediaType"text/vnd.api+json"The interpolator reuses predefined instances when available (reference equality with MediaTypes constants). It does not support variable interpolation — only literal strings are accepted.
:::note
The mediaType interpolator is available after importing zio.blocks.mediatype._. Invalid inputs produce clear compile-time errors:
mediaType"" → "Invalid media type: cannot be empty"
mediaType"applicationjson" → "Invalid media type: must contain '/' separator"
mediaType"/json" → "Invalid media type: main type cannot be empty"
mediaType"application/" → "Invalid media type: subtype cannot be empty"
:::
MediaType.forFileExtension finds a MediaType by its associated file extension. The lookup is case-insensitive and strips a leading . if present:
import zio.blocks.mediatype.MediaType
MediaType.forFileExtension("json") // Some(MediaType("application", "json", ...))
MediaType.forFileExtension(".html") // Some(MediaType("text", "html", ...))
MediaType.forFileExtension("PNG") // Some(MediaType("image", "png", ...))
MediaType.forFileExtension("jpg") // Some(MediaType("image", "jpeg", ...))
MediaType.forFileExtension("pdf") // Some(MediaType("application", "pdf", ...))
MediaType.forFileExtension("xyz123") // None (unknown extension)
MediaType.forFileExtension("") // None
MediaType.forFileExtension(".") // NoneThe MediaTypes object contains 2,600+ predefined media type constants auto-generated from the jshttp/mime-db database. Types are organized by main type category:
| Category | Object | Examples |
|---|---|---|
| Application | MediaTypes.application |
json, pdf, xml, octet-stream |
| Audio | MediaTypes.audio |
mpeg, ogg, wav |
| Chemical | MediaTypes.chemical |
x-cif, x-pdb |
| Font | MediaTypes.font |
woff, woff2, otf |
| Image | MediaTypes.image |
png, jpeg, gif, svg+xml |
| Message | MediaTypes.message |
rfc822, partial |
| Model | MediaTypes.model |
gltf+json, stl |
| Multipart | MediaTypes.multipart |
form-data, mixed |
| Text | MediaTypes.text |
html, css, plain, csv |
| Video | MediaTypes.video |
mp4, webm, ogg |
| X-Conference | MediaTypes.x_conference |
x-cooltalk |
| X-Shader | MediaTypes.x_shader |
x-vertex, x-fragment |
| Wildcard | MediaTypes.any |
*/* |
Since many subtype names contain special characters (hyphens, dots, plus signs), predefined constants use backtick identifiers:
import zio.blocks.mediatype.MediaTypes
// Common application types
val json = MediaTypes.application.`json`
val pdf = MediaTypes.application.`pdf`
val xmlType = MediaTypes.application.`xml`
// Common text types
val html = MediaTypes.text.`html`
val css = MediaTypes.text.`css`
val plain = MediaTypes.text.`plain`
// Common image types
val png = MediaTypes.image.`png`
val jpeg = MediaTypes.image.`jpeg`
// Wildcard (matches any type)
val any = MediaTypes.anyEach category object has an all field returning a List[MediaType] of all types in that category. The top-level allMediaTypes aggregates every category:
import zio.blocks.mediatype.MediaTypes
// All types in a category
val appTypes: List[zio.blocks.mediatype.MediaType] = MediaTypes.application.all
// All 2,600+ predefined types
val everything: List[zio.blocks.mediatype.MediaType] = MediaTypes.allMediaTypesEach predefined instance comes with rich metadata from the IANA registry:
import zio.blocks.mediatype.MediaTypes
val json = MediaTypes.application.`json`
json.compressible // true
json.binary // false
json.fileExtensions // List("json", "map")
val jpeg = MediaTypes.image.`jpeg`
jpeg.compressible // false
jpeg.binary // true
jpeg.fileExtensions // List("jpg", "jpeg", "jpe")
val html = MediaTypes.text.`html`
html.compressible // true
html.binary // false
html.fileExtensions // List("html", "htm", "shtml")Returns the complete media type string by combining mainType and subType with a / separator:
import zio.blocks.mediatype.MediaType
val mt = MediaType("application", "json")
mt.fullType // "application/json"
MediaType("*", "*").fullType // "*/*"
MediaType("text", "*").fullType // "text/*"Compares two MediaType values with support for wildcards, case insensitivity, and parameter matching:
final case class MediaType(...) {
def matches(other: MediaType, ignoreParameters: Boolean = false): Boolean
}The matching rules are:
- Wildcard matching —
"*"in eithermainTypeorsubTypematches any value - Case-insensitive —
"APPLICATION/JSON"matches"application/json" - Parameter subset — when
ignoreParameters = false(the default), all parameters inthismust exist inotherwith matching values (case-insensitive). Extra parameters inotherare allowed.
import zio.blocks.mediatype._
val json = mediaType"application/json"
val textAll = mediaType"text/*"
val html = mediaType"text/html"
val any = mediaType"*/*"
// Exact match
json.matches(json) // true
// Wildcard matching
any.matches(json) // true — */* matches anything
textAll.matches(html) // true — text/* matches text/html
textAll.matches(json) // false — text/* does not match application/json
// Case-insensitive
MediaType("APPLICATION", "JSON").matches(json) // true
// Parameter matching
val htmlUtf8 = MediaType("text", "html", parameters = Map("charset" -> "utf-8"))
val htmlLatin = MediaType("text", "html", parameters = Map("charset" -> "iso-8859-1"))
val htmlFull = MediaType("text", "html", parameters = Map("charset" -> "utf-8", "boundary" -> "xxx"))
htmlUtf8.matches(htmlLatin) // false — charset mismatch
htmlUtf8.matches(htmlLatin, ignoreParameters = true) // true — parameters ignored
htmlUtf8.matches(htmlFull) // true — subset match (charset matches)Looks up a MediaType by file extension. The lookup is case-insensitive and strips a leading . if present:
object MediaType {
def forFileExtension(ext: String): Option[MediaType]
}import zio.blocks.mediatype.MediaType
MediaType.forFileExtension("json") // Some(MediaType("application", "json", ...))
MediaType.forFileExtension(".html") // Some(MediaType("text", "html", ...))
MediaType.forFileExtension("PNG") // Some(MediaType("image", "png", ...))
MediaType.forFileExtension("") // None
MediaType.forFileExtension("xyz") // None:::tip
forFileExtension gives priority to text/* types when an extension maps to multiple types. For example, "js" maps to text/javascript rather than application/javascript.
:::
Parses a media type string into a MediaType, returning Left with an error message for invalid input:
object MediaType {
def parse(s: String): Either[String, MediaType]
}When the parsed type matches a predefined instance, that instance is returned (preserving reference equality and all metadata). Parameters from the input string are merged into the result:
import zio.blocks.mediatype.{MediaType, MediaTypes}
// Returns predefined instance with full metadata
val json = MediaType.parse("application/json")
// Right(MediaType("application", "json", compressible=true, binary=false, ...))
// Parameters are parsed and attached
val result = MediaType.parse("multipart/form-data; boundary=abc; charset=utf-8")
// Right(MediaType(..., parameters = Map("boundary" -> "abc", "charset" -> "utf-8")))
// Unknown types get a fresh instance
val custom = MediaType.parse("custom/x-my-format")
// Right(MediaType("custom", "x-my-format"))
// Malformed parameters (no "=") are silently ignored
val partial = MediaType.parse("text/html; charset=utf-8; malformed")
// Right(MediaType(..., parameters = Map("charset" -> "utf-8")))Like parse, but throws IllegalArgumentException instead of returning Left:
object MediaType {
def unsafeFromString(s: String): MediaType
}For valid input, it returns the corresponding MediaType (reusing predefined instances when possible). For invalid input, it throws an exception with a descriptive message:
import zio.blocks.mediatype.MediaType
val json = MediaType.unsafeFromString("application/json")
// Throws IllegalArgumentException:
// MediaType.unsafeFromString("not-a-media-type")We can use matches with wildcard types to implement HTTP-style content negotiation:
import zio.blocks.mediatype._
def negotiate(
accept: List[MediaType],
available: List[MediaType]
): Option[MediaType] =
available.find(avail => accept.exists(_.matches(avail, ignoreParameters = true)))
val accept = List(mediaType"text/*", mediaType"application/json")
val available = List(mediaType"application/xml", mediaType"application/json", mediaType"text/html")
negotiate(accept, available)
// Some(MediaType("application", "json", ...))Combine forFileExtension with file path processing to detect content types:
import zio.blocks.mediatype.MediaType
def detectContentType(filename: String): Option[MediaType] = {
val ext = filename.lastIndexOf('.') match {
case -1 => ""
case i => filename.substring(i + 1)
}
MediaType.forFileExtension(ext)
}
detectContentType("report.pdf") // Some(MediaType("application", "pdf", ...))
detectContentType("photo.jpg") // Some(MediaType("image", "jpeg", ...))
detectContentType("data.json") // Some(MediaType("application", "json", ...))
detectContentType("Makefile") // None