Vix.cpp v2.6.0 is here Read the blog
Skip to content

Parsers

The parsers group turns raw HTTP request bodies into typed data your handlers can use safely.

A backend should not parse the same input manually in every route.

The parser middleware handles reusable request-body work:

txt
check Content-Type
check body size
parse the body
store typed request state
stop invalid requests before the handler

Then the route handler reads typed state and focuses on application logic.

The parser middleware lives under:

cpp
namespace vix::middleware::parsers

When using vix::App, prefer the App helpers:

cpp
namespace vix::middleware::app

What parsers provides

The parser group includes:

MiddlewarePurpose
json()Parse application/json bodies
form()Parse application/x-www-form-urlencoded bodies
multipart()Validate multipart metadata and boundary
multipart_save()Parse multipart form-data and save uploaded files

For normal vix::App applications, use the App presets:

cpp
middleware::app::json_dev(...)
middleware::app::json_strict_dev(...)
middleware::app::form_dev(...)
middleware::app::multipart_dev(...)
middleware::app::multipart_save_dev(...)

Why parsers matter

Without parser middleware, every route must do this manually:

txt
read raw body
check Content-Type
handle empty body
enforce max size
parse JSON or form data
handle parse errors
return consistent error responses

With parser middleware, this reusable work happens before the handler.

cpp
app.use("/api/users", middleware::app::json_strict_dev(4096));

app.post("/api/users", [](vix::Request &req, vix::Response &res)
{
  auto &body = req.state<vix::middleware::parsers::JsonBody>();

  res.json({
    "ok", true,
    "received", body.value.dump()
  });
});

The handler can assume that the body was already parsed.

If parsing fails, the handler is not called.

Parser middleware should usually run after security and body size checks.

cpp
app.use("/api", middleware::app::security_headers_dev());
app.use("/api", middleware::app::cors_dev());
app.use("/api", middleware::app::rate_limit_dev());
app.use("/api", middleware::app::body_limit_write_dev(1024 * 1024));

app.use("/api/users", middleware::app::json_strict_dev(4096));

The order matters.

txt
rate limit
  rejects abusive clients

body limit
  rejects oversized bodies

parser
  parses only valid-sized bodies

handler
  uses typed state

Avoid parsing large or invalid requests before they have passed basic limits.

JSON parser

json() parses JSON request bodies and stores:

cpp
vix::middleware::parsers::JsonBody

Use JSON parsing for API routes.

The most common App preset is:

cpp
app.use("/api/users", middleware::app::json_strict_dev(4096));

json_strict_dev() is useful when a route requires:

txt
Content-Type: application/json
non-empty body
valid JSON
body size under the configured limit

JSON example

cpp
#include <vix.hpp>
#include <vix/middleware.hpp>

using namespace vix;

int main()
{
  App app;

  app.use("/api/users", middleware::app::json_strict_dev(4096));

  app.post("/api/users", [](Request &req, Response &res)
  {
    auto &body = req.state<middleware::parsers::JsonBody>();

    const std::string name = body.value.value("name", "");
    const std::string email = body.value.value("email", "");

    if (name.empty())
    {
      res.status(422).json({
        "ok", false,
        "error", "Missing name"
      });
      return;
    }

    if (email.empty())
    {
      res.status(422).json({
        "ok", false,
        "error", "Missing email"
      });
      return;
    }

    res.status(201).json({
      "ok", true,
      "user", {
        "name", name,
        "email", email
      }
    });
  });

  app.run(8080);
}

Run:

bash
vix run json_parser_demo.cpp

Send valid JSON:

bash
curl -i \
  -X POST http://127.0.0.1:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Ada","email":"ada@example.com"}'

Expected status:

txt
201 Created

Send invalid JSON:

bash
curl -i \
  -X POST http://127.0.0.1:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":}'

Expected status:

txt
400 Bad Request

Send wrong content type:

bash
curl -i \
  -X POST http://127.0.0.1:8080/api/users \
  -H "Content-Type: text/plain" \
  -d '{"name":"Ada"}'

Expected status:

txt
415 Unsupported Media Type

Send empty body:

bash
curl -i \
  -X POST http://127.0.0.1:8080/api/users \
  -H "Content-Type: application/json" \
  -d ''

Expected status with strict JSON:

txt
400 Bad Request

Strict JSON vs relaxed JSON

Use strict JSON when the route requires a body.

cpp
app.use("/api/users", middleware::app::json_strict_dev(4096));

Use relaxed JSON when an empty body is acceptable.

cpp
app.use("/api/search", middleware::app::json_dev(
  4096,
  true,
  true
));

The practical rule is:

Route typeParser
POST /api/usersjson_strict_dev(...)
PUT /api/users/:idjson_strict_dev(...)
PATCH /api/users/:idjson_strict_dev(...)
Optional filter bodyjson_dev(...)
Health or GET routeNo JSON parser

Do not install JSON parsing globally unless every route under that prefix expects JSON.

Prefer route-specific parser prefixes.

JSON options

Use lower-level options for exact behavior.

cpp
vix::middleware::parsers::JsonParserOptions opt;

opt.require_content_type = true;
opt.allow_empty = false;
opt.max_bytes = 4096;
opt.store_in_state = true;

app.use("/api/json", vix::middleware::app::adapt_ctx(
  vix::middleware::parsers::json(opt)
));

Main options:

OptionPurpose
require_content_typeRequire Content-Type: application/json
allow_emptyAllow empty body and store {}
max_bytesMaximum body size for the parser
store_in_stateStore JsonBody in request state

Common JSON errors:

StatusCodeMeaning
400empty_bodyBody is required but empty
400invalid_jsonBody could not be parsed as JSON
413payload_too_largeBody exceeds parser limit
415unsupported_media_typeContent type is not JSON

Form parser

form() parses:

txt
application/x-www-form-urlencoded

It stores:

cpp
vix::middleware::parsers::FormBody

Use it for classic HTML forms and simple form posts.

Form example

cpp
#include <vix.hpp>
#include <vix/middleware.hpp>

using namespace vix;

int main()
{
  App app;

  app.use("/form", middleware::app::form_dev(1024));

  app.get("/", [](Request &, Response &res)
  {
    res.text("POST /form with application/x-www-form-urlencoded");
  });

  app.post("/form", [](Request &req, Response &res)
  {
    auto &form = req.state<middleware::parsers::FormBody>();

    const auto name = form.fields.find("name");

    if (name == form.fields.end() || name->second.empty())
    {
      res.status(422).json({
        "ok", false,
        "error", "Missing name"
      });
      return;
    }

    res.json({
      "ok", true,
      "name", name->second
    });
  });

  app.run(8080);
}

Test:

bash
curl -i \
  -X POST http://127.0.0.1:8080/form \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "name=Ada&city=Kampala"

Expected status:

txt
200 OK

Wrong content type:

bash
curl -i \
  -X POST http://127.0.0.1:8080/form \
  -H "Content-Type: text/plain" \
  --data "name=Ada"

Expected status:

txt
415 Unsupported Media Type

Form options

Use lower-level options when needed.

cpp
vix::middleware::parsers::FormParserOptions opt;

opt.require_content_type = true;
opt.max_bytes = 1024;
opt.store_in_state = true;

app.use("/form", vix::middleware::app::adapt_ctx(
  vix::middleware::parsers::form(opt)
));

Main options:

OptionPurpose
require_content_typeRequire URL-encoded form content type
max_bytesMaximum body size
store_in_stateStore FormBody in request state

Common form errors:

StatusCodeMeaning
413payload_too_largeBody exceeds parser limit
415unsupported_media_typeContent type is not URL-encoded form

Multipart parser

multipart() validates multipart request metadata.

It checks:

txt
Content-Type starts with multipart/form-data
boundary exists when required
body size does not exceed the configured limit

It stores:

cpp
vix::middleware::parsers::MultipartInfo

This parser is useful when you only need to validate multipart metadata.

For real file uploads, use multipart_save() through the App helper.

Multipart save

multipart_save() handles multipart form-data and saves uploaded files.

The App preset is usually:

cpp
app.use("/upload", middleware::app::multipart_save_dev("uploads"));

It stores a parsed multipart form in request state.

cpp
vix::middleware::parsers::MultipartForm

A handler can then read the parsed form and return a response.

Multipart upload example

cpp
#include <vix.hpp>
#include <vix/middleware.hpp>

using namespace vix;

int main()
{
  App app;

  app.use("/upload", middleware::app::multipart_save_dev("uploads"));

  app.get("/", [](Request &, Response &res)
  {
    res.text("POST /upload with multipart/form-data");
  });

  app.post("/upload", [](Request &req, Response &res)
  {
    auto &form = req.state<middleware::parsers::MultipartForm>();

    res.json(middleware::app::multipart_json(form));
  });

  app.run(8080);
}

Run:

bash
vix run multipart_upload_demo.cpp

Upload fields:

bash
curl -i \
  -X POST http://127.0.0.1:8080/upload \
  -F "title=Profile" \
  -F "description=Avatar upload"

Upload a file:

bash
curl -i \
  -X POST http://127.0.0.1:8080/upload \
  -F "title=Avatar" \
  -F "file=@./avatar.png"

The middleware saves uploaded files into:

txt
uploads/

The handler receives the parsed multipart state.

Multipart with CORS

If a browser frontend uploads files from another origin, install CORS before multipart parsing.

cpp
app.use("/upload", middleware::app::cors_dev({"http://localhost:5173"}));
app.use("/upload", middleware::app::multipart_save_dev("uploads"));

app.options("/upload", [](Request &, Response &res)
{
  res.status(204).send();
});

The CORS middleware must be able to answer preflight requests before upload parsing runs.

Multipart options

Use lower-level options when you only need metadata validation.

cpp
vix::middleware::parsers::MultipartOptions opt;

opt.require_boundary = true;
opt.max_bytes = 1024 * 1024;
opt.store_in_state = true;

app.use("/multipart", vix::middleware::app::adapt_ctx(
  vix::middleware::parsers::multipart(opt)
));

Main options:

OptionPurpose
require_boundaryRequire the multipart boundary
max_bytesMaximum body size
store_in_stateStore MultipartInfo in request state

Common multipart errors:

StatusCodeMeaning
400missing_boundaryMultipart boundary is missing
413payload_too_largeBody exceeds parser limit
415unsupported_media_typeContent type is not multipart form-data

Choosing the right parser

Use this rule:

Request bodyMiddleware
JSON API bodyjson_strict_dev(...)
Optional JSON bodyjson_dev(...)
HTML form postform_dev(...)
Multipart metadata checkmultipart(...)
File uploadsmultipart_save_dev(...)

Install parsers only where the route expects that body format.

Good:

cpp
app.use("/api/users", middleware::app::json_strict_dev(4096));
app.use("/upload", middleware::app::multipart_save_dev("uploads"));

Avoid:

cpp
app.use("/", middleware::app::json_strict_dev(4096));

A global strict parser can reject routes that do not have request bodies.

Parsers and typed state

Every parser stores a typed object.

ParserState type
json()JsonBody
form()FormBody
multipart()MultipartInfo
multipart_save()MultipartForm

Read state in the handler:

cpp
auto &body = req.state<vix::middleware::parsers::JsonBody>();

Use state<T>() when the parser must exist.

Use try_state<T>() when the parser may be optional.

cpp
auto *body = req.try_state<vix::middleware::parsers::JsonBody>();

if (!body)
{
  res.status(500).json({
    "ok", false,
    "error", "json_state_missing"
  });
  return;
}

Parsers and validation

Parsers are not full business validators.

A parser answers:

txt
Is the body syntactically valid?
Can it be decoded?
Can the result be stored?

Application validation answers:

txt
Is this field required?
Is this email valid?
Is this quantity positive?
Is this product available?

Example:

cpp
app.post("/api/orders", [](vix::Request &req, vix::Response &res)
{
  auto &body = req.state<vix::middleware::parsers::JsonBody>();

  const int quantity = body.value.value("quantity", 0);

  if (quantity <= 0)
  {
    res.status(422).json({
      "ok", false,
      "error", "Quantity must be positive"
    });
    return;
  }

  res.status(201).json({
    "ok", true
  });
});

The parser validates the body format.

The handler validates application rules.

Parsers and body limits

Use body limits before parsers.

cpp
app.use("/api", middleware::app::body_limit_write_dev(1024 * 1024));
app.use("/api/users", middleware::app::json_strict_dev(4096));

This avoids doing parser work on oversized requests.

A parser can still have its own max_bytes.

The body limit protects the broader route group.

The parser limit protects the specific body format.

Complete example

This example combines JSON, form data, and multipart upload routes.

cpp
#include <vix.hpp>
#include <vix/middleware.hpp>

using namespace vix;

int main()
{
  App app;

  app.use("/api", middleware::app::security_headers_dev());
  app.use("/api", middleware::app::rate_limit_dev());
  app.use("/api", middleware::app::body_limit_write_dev(1024 * 1024));

  app.use("/api/users", middleware::app::json_strict_dev(4096));
  app.use("/form", middleware::app::form_dev(4096));
  app.use("/upload", middleware::app::multipart_save_dev("uploads"));

  app.post("/api/users", [](Request &req, Response &res)
  {
    auto &body = req.state<middleware::parsers::JsonBody>();

    res.status(201).json({
      "ok", true,
      "json", body.value.dump()
    });
  });

  app.post("/form", [](Request &req, Response &res)
  {
    auto &form = req.state<middleware::parsers::FormBody>();

    res.json({
      "ok", true,
      "fields_count", static_cast<long long>(form.fields.size())
    });
  });

  app.post("/upload", [](Request &req, Response &res)
  {
    auto &form = req.state<middleware::parsers::MultipartForm>();

    res.json(middleware::app::multipart_json(form));
  });

  app.run(8080);
}

Test JSON:

bash
curl -i \
  -X POST http://127.0.0.1:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Ada"}'

Test form:

bash
curl -i \
  -X POST http://127.0.0.1:8080/form \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "name=Ada&city=Kampala"

Test multipart:

bash
curl -i \
  -X POST http://127.0.0.1:8080/upload \
  -F "title=Avatar" \
  -F "file=@./avatar.png"

Summary

Use parser middleware to keep handlers focused.

A parser should:

txt
check the request body format
reject invalid input early
store typed request state
let the handler work with parsed data

A good default pattern is:

cpp
app.use("/api", middleware::app::body_limit_write_dev(1024 * 1024));
app.use("/api/users", middleware::app::json_strict_dev(4096));

Then inside the handler:

cpp
auto &body = req.state<middleware::parsers::JsonBody>();

Remember:

txt
body limit before parser
parser before handler
business validation inside the handler or validation layer

Released under the MIT License.