ol.sfv
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:
2853 tests, 3220 assertions
){:deps {com.outskirtslabs/sfv {:mvn/version "0.1.0"}}}
;; Leiningen
[com.outskirtslabs/sfv "0.1.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-item {: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-item {: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 | 42 , -17 , 999999999999999 | {:type :integer :value 1618884473} | long |
Decimal | 3.14 , -0.5 | {:type :decimal :value 3.14M} | BigDecimal |
String | "hello world" | {:type :string :value "hello"} | java.lang.String |
Token | simple-token | {:type :token :value "simple-token"} | String |
Byte Sequence | :SGVsbG8=: | {:type :bytes :value <platform bytes>} | byte[] |
Boolean | ?1 / ?0 | {:type :boolean :value true} | true / false |
Date | @1659578233 | {:type :date :value 1659578233} | epoch seconds as long |
Display String | %"Gr%c3%bc%c3%9fe" | {:type :display :value "Grüße"} | String (percent-decoded, validated) |
BigDecimal
to avoid float rounding.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:
See here for security advisories or to report a security vulnerability.
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 |