;; deps.edn
{:deps {com.outskirtslabs/sfv {:mvn/version "0.2.0"}}}
;; Leiningen
[com.outskirtslabs/sfv "0.2.0"]
A 0-dependency Clojure library for parsing and generating Structured Field Values for HTTP (RFC 9651/8941)
Structured Field Values (SFV) as defined in RFC 9651 provide a standardized way to encode complex data structures in HTTP headers.
ol.sfv library implements the specification, providing parsing and
serialization capabilities.
This is a low-level library that emits and consumes the RFC 9651 AST,
which unfortunately does not map cleanly to Clojure datastructures. For
practical use, it should be wrapped in higher-level functions or
libraries that implement specific HTTP headers like
Permissions-Policy, Signature-Input, or Signature headers (any any
future HTTP headers).
Key features:
Complete RFC 9651 implementation with AST-level parsing and serialization with zero deps
Extensively tested (2853 tests, 3225 assertions)
Precise round-trip fidelity - parse and serialize back to identical strings
Validation and error reporting
JVM/Graal target
Project status: Stable.
;; deps.edn
{:deps {com.outskirtslabs/sfv {:mvn/version "0.2.0"}}}
;; Leiningen
[com.outskirtslabs/sfv "0.2.0"]
(ns myapp.core
(:require [ol.sfv :as sfv]))
;; Integer
(sfv/parse-item "42")
{:type :item :bare {:type :integer :value 42} :params []}
;; They are round trippable
(sfv/serialize {:type :item :bare {:type :integer :value 42}})
;; => "42"
;; Display String
(sfv/parse-item "%\"Gr%c3%bc%c3%9fe\"")
{:type :item :bare {:type :dstring :value "Grüße"} :params []}
(sfv/serialize {:type :item :bare {:type :dstring :value "السلام عليكم"} :params []})
"%\"%d8%a7%d9%84%d8%b3%d9%84%d8%a7%d9%85 %d8%b9%d9%84%d9%8a%d9%83%d9%85\""
;; Dates
(sfv/parse-item "@1659578233")
{:type :item, :bare {:type :date, :value 1659578233}, :params []}
;; Items can have params attached
(sfv/parse-item "pear;sweet=?1")
{:type :item
:bare {:type :token :value "pear"}
:params [["sweet" {:type :token :value true}]]}
;; Parse a list with parameters
(sfv/parse-list "apple, pear;sweet=true, orange")
{:type :list
:members [{:type :item :bare {:type :token :value "apple"} :params []}
{:type :item :bare {:type :token :value "pear"} :params [["sweet" {:type :token :value "true"}]]}
{:type :item :bare {:type :token :value "orange"} :params []}]}
;; Dictionaries
(sfv/parse-dict "max-age=3600, must-revalidate")
{:type :dict
:entries [["max-age" {:type :item :bare {:type :integer :value 3600} :params []}]
["must-revalidate" {:type :item :bare {:type :boolean :value true} :params []}]]}
;; Dictionaries with inner lists and params
(sfv/parse-dict "trees=(\"spruce\";type=conifer \"oak\";type=deciduous)")
{:type :dict
:entries [["trees"
{:type :inner-list
:items [{:type :item :bare {:type :string :value "spruce"}
:params [["type" {:type :token :value "conifer"}]]}
{:type :item :bare {:type :string :value "oak"}
:params [["type" {:type :token :value "deciduous"}]]}]
:params []}]]}
(sfv/parse-dict "foods=(\"burger\";sandwich=?1 \"pizza\";sandwich=?0 \"hot dog\";sandwich=?1);comprehensive=?0")
{:type :dict
:entries [["foods" {:type :inner-list
:items [{:type :item :bare {:type :string :value "burger"}
:params [["sandwich" {:type :boolean :value true}]]}
{:type :item :bare {:type :string :value "pizza"}
:params [["sandwich" {:type :boolean :value false}]]}
{:type :item :bare {:type :string :value "hot dog"}
:params [["sandwich" {:type :boolean :value true}]]}]
:params [["comprehensive" {:type :boolean :value false}]]}]]}
;; List with Items and Inner List
(sfv/parse-list "circle;color=red, square;filled=?0, (triangle;size=3 rectangle;size=4)")
{:type :list
:members [{:type :item :bare {:type :token :value "circle"}
:params [["color" {:type :token :value "red"}]]}
{:type :item :bare {:type :token :value "square"}
:params [["filled" {:type :boolean :value false}]]}
{:type :inner-list :items [{:type :item :bare {:type :token :value "triangle"}
:params [["size" {:type :integer :value 3}]]}
{:type :item :bare {:type :token :value "rectangle"}
:params [["size" {:type :integer :value 4}]]}]
:params []}]}
ol.sfv is a low-level, AST-oriented implementation of RFC 9651. We
don’t try to coerce values into “nice” Clojure shapes; instead we
expose a precise tree that round-trips byte-for-byte. This is important
because Structured Fields carry ordering information that ordinary
Clojure maps can’t reliably preserve across platforms and sizes.
RFC 9651 defines several primitive types, all supported.
Each Item’s :bare is one of the following:
| SFV type | Header | AST example | Clojure type (:value) |
|---|---|---|---|
Integer |
|
|
|
Decimal |
|
|
|
String |
|
|
|
Token |
|
|
|
Byte Sequence |
|
|
|
Boolean |
|
|
|
Date |
|
| epoch
seconds as |
Display String |
|
|
|
Decimals obey SFV’s constraints (≤3 fractional digits, length limits)
and are parsed to BigDecimal to avoid float rounding.
Byte sequences are base64 inside the header; we give you the decoded bytes.
Item — a bare value plus optional parameters
{:type :item
:bare <bare-ast>
:params [ [param-name <bare-ast-or-true>] ... ]}
List — a sequence of items or inner lists
{:type :list
:members [ <item-or-inner-list> ... ]}
Dictionary — an ordered sequence of key→member entries
{:type :dict
:entries [ [key <item-or-inner-list>] ... ]}
Inner List — a parenthesized list (appears inside List/Dictionary) with its own parameters
{:type :inner-list
:items [ <item> ... ]
:params [ [param-name <bare-ast-or-true>] ... ]}
Keys are parsed as lower-case identifiers per the spec.
Dictionaries and parameter lists in SFV have a defined member order. That order may or may not be semantically meaningful, that depends on the specific header.
To make ordering explicit and stable, we represent dictionaries and parameter lists as vectors of [k v] pairs. That’s portable, preserves order, and lets you implement whatever key semantics you need at a higher level.
Parameters attach metadata to an Item or an Inner List:
Syntax: ;key[=value] repeating after the base value If
=value is omitted, the parameter value is the boolean true.
In the AST, parameters are always a vector of [name value]
pairs in the order seen:
(sfv/parse-item "pear;sweet=?1")
{:type :item
:bare {:type :token :value "pear"}
:params [["sweet" {:type :boolean :value true}]]}
Parameter values are bare values (not nested items). You’ll see the
same :type/:value shapes as the table above
Ordering is preserved for round-trip fidelity
Dictionaries map from a key to either an Item or an Inner List. We keep them as an ordered vector of entries:
(sfv/parse-dict "max-age=3600, must-revalidate")
{:type :dict
:entries [["max-age" {:type :item :bare {:type :integer :value 3600} :params []}]
["must-revalidate" {:type :item :bare {:type :boolean :value true} :params []}]]}
This library is designed to be wrapped by more specific implementations:
(ns myapp.cache-control
(:require [ol.sfv :as sfv]))
(defn parse-cache-control [header-value]
(let [parsed (sfv/parse-dict header-value)]
(reduce (fn [acc [key item]]
(let [value (get-in item [:bare :value])]
(assoc acc (keyword key) value)))
{}
(:entries parsed))))
(parse-cache-control "max-age=3600, must-revalidate")
;; => {:max-age 3600, :must-revalidate true}
Structured Field Values provide a robust foundation for modern HTTP header design:
RFC 9651: Structured Field Values for HTTP - The complete specification
RFC 9421: HTTP Message Signatures - A major consumer of structured fields
Copyright © 2025 Casey Link casey@outskirtslabs.com
Distributed under the MIT License
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |