hermes-json: Fast JSON decoding via simdjson C++ bindings

[ ffi, json, library, mit, text, web ] [ Propose Tags ] [ Report a vulnerability ]

A JSON parsing library focused on speed that binds to the simdjson C++ library using the Haskell FFI. Hermes offers some helpful functions for building fast JSON decoders for your Haskell types.


[Skip to Readme]

Flags

Manual Flags

NameDescriptionDefault
strict

Enable more GHC warnings plus -Werror, which turns warnings into errors.

Disabled
native_comp

Target native architecture for C++ compiler

Disabled
debug

Enable C++ debug support

Disabled

Use -f <flag> to enable a flag, or -f -<flag> to disable that flag. More info

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 0.1.0.0, 0.1.0.1, 0.2.0.0, 0.2.0.1, 0.3.0.0, 0.4.0.0, 0.5.0.0, 0.6.0.0, 0.6.1.0, 0.7.0.0, 0.8.0.0, 0.8.1.0, 0.8.2.0, 0.9.0.0
Change log CHANGELOG.md
Dependencies base (>=4.13 && <4.23), bytestring (>=0.10.12 && <0.13), containers (>=0.6.5 && <0.9), deepseq (>=1.4.4 && <1.6), dlist (>=0.8 && <1.1), integer-conversion (>=0.1 && <0.2), primitive (>=0.7.0 && <0.10), scientific (>=0.3.6 && <0.4), system-cxx-std-lib (==1.0), text (>=2.0 && <2.2), text-iso8601 (>=0.2 && <0.3), time (>=1.9.3 && <1.16), time-compat (>=1.9.5 && <1.10), transformers (>=0.5.6 && <0.7), vector (>=0.12.3.1 && <0.14) [details]
Tested with ghc ==9.10.3, ghc ==9.12.3, ghc ==9.14.1
License MIT
Author Josh Miller <joshua.ian.miller@pm.me>
Maintainer Josh Miller <joshua.ian.miller@pm.me>
Uploaded by velveteer at 2026-10-14T21:19:28Z
Category Text, Web, JSON, FFI
Home page https://github.com/velveteer/hermes
Source repo head: git clone git@github.com:velveteer/hermes.git
Distributions
Reverse Dependencies 1 direct, 0 indirect [details]
Downloads 1374 total (55 in the last 30 days)
Rating 2.5 (votes: 3) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs available [build log]
Last success reported on 2026-10-14 [all 1 reports]

Readme for hermes-json-0.9.0.0

[back to package description]

hermes

CI badge Hackage badge

A Haskell interface over the simdjson C++ library for decoding JSON documents. Hermes, messenger of the gods, was the maternal great-grandfather of Jason, son of Aeson.

Overview

This library exposes functions that can be used to write decoders for JSON documents using the simdjson On Demand API. From the simdjson On Demand design documentation:

Good applications for the On Demand API might be:

You are working from pre-existing large JSON files that have been vetted. You expect them to be well formed according to a known JSON dialect and to have a consistent layout. For example, you might be doing biomedical research or machine learning on top of static data dumps in JSON.

Both the generation and the consumption of JSON data is within your system. Your team controls both the software that produces the JSON and the software the parses it, your team knows and control the hardware. Thus you can fully test your system.

You are working with stable JSON APIs which have a consistent layout and JSON dialect.

With this in mind, Data.Hermes parsers can decode Haskell types faster than traditional Data.Aeson.FromJSON instances, especially in cases where you only need to decode a subset of the document. This is because Data.Aeson.FromJSON converts the entire document into a Data.Aeson.Value, which means memory usage increases linearly with the input size. The simdjson::ondemand API does not have this constraint because it iterates over the JSON string in memory without constructing an intermediate tree. This means decoders are truly lazy and you only pay for what you use.

Hermes requires the entire document in memory. For an incremental JSON parser that supports streaming, see json-stream.

Usage

This library does not offer a Haskell API over the entire simdjson On Demand API. It currently binds only to what is needed for defining and running a Decoder. You can see the tests and benchmarks for example usage. simdjson::ondemand exceptions will be caught and re-thrown with enough information to troubleshoot. In the worst case you may run into a segmentation fault that is not caught, which you are encouraged to report as a bug.

Decoders

import qualified Data.ByteString as BS
import qualified Data.Hermes as H

personDecoder :: H.Decoder Person
personDecoder = H.object $
  Person
    <$> H.atKey "_id" H.text
    <*> H.atKey "index" H.int
    <*> H.atKey "guid" H.text
    <*> H.atKey "isActive" H.bool
    <*> H.atKey "balance" H.text
    <*> H.atKey "picture" (H.nullable H.text)
    <*> H.atKey "latitude" H.scientific

-- Decode a strict ByteString.
decodePersons :: BS.ByteString -> Either H.HermesException [Person]
decodePersons = H.decodeEither $ H.list personDecoder

Aeson Integration

While it is not recommended to use hermes if you need the full DOM, we still provide a performant interface to decode aeson Values. See an example of this in the hermes-aeson subpackage. You could use hermes to selectively decode aeson Values on demand, for example:

> decodeEither (atPointer "/statuses/99/user/screen_name" hValueToAeson) twitter
Right (String "2no38mae")

Exceptions

When decoding fails for a known reason, you will get a Left HermesException indicating if the error came from simdjson or from an internal hermes call.

> decodeEither (object . atKey "hello" $ list text) "{ \"hello\": [\"world\", false] }"
Left (SIMDException (DocumentError {path = "/hello/1", errorMsg = "Error while getting value of type text. INCORRECT_TYPE: The JSON element does not have the requested type."}))

Benchmarks

We benchmark the following operations using both hermes-json and aeson strict ByteString decoders:

  • Decode a small array of 3-element arrays of doubles
  • Full decoding of a large-ish (12 MB) JSON array of Person objects
  • Partial decoding of Twitter status objects to highlight the on-demand benefits
  • Decoding entire documents into Data.Aeson.Value

Please be aware that GHC does not report C-allocated memory. simdjson does actually allocate more memory than appears here, but we still strive to keep our Haskell memory footprint as small as possible.

Specs

  • GHC 9.14.1 w/ -O1
  • aeson-2.3
  • Apple M5 Pro

Arrays

Persons

Partial Twitter

Persons (Aeson Value)

Twitter (Aeson Value)

Name Mean (ps) 2*Stdev (ps) Allocated Copied Peak Memory
All.Arrays.Hermes 736661914 56835868 4320997 49336 78643200
All.Arrays.Aeson 11665968750 935375184 71682940 1967960 78643200
All.Persons.Hermes 14243446875 1005063640 52984155 9592134 126877696
All.Persons.Aeson 82948100000 8126094636 358899523 125070437 261095424
All.Partial Twitter.Hermes 125155444 3923382 195772 149 75497472
All.Partial Twitter.Aeson 1779148046 152850968 12796011 220186 74448896
All.Partial Twitter.JsonStream 1732640625 156437412 16170009 14820 90177536
All.Persons (Aeson Value).Hermes 42076150000 4106762446 152298884 70268110 168820736
All.Persons (Aeson Value).Aeson 67366093750 2380713624 285929614 92447746 239075328
All.Twitter (Aeson Value).Hermes 1127491406 93825760 7282920 122405 75497472
All.Twitter (Aeson Value).Aeson 1857667968 114563544 12589248 205953 74448896

Performance Tips

  • Decode to Text instead of String wherever possible!
  • Decode to Int or Double instead of Scientific if you can.
  • Decode your object fields in order. If encoding with aeson, you can leverage toEncoding to enforce ordering.
  • In tight loops or servers, reuse a HermesEnv with parseByteString instead of decodeEither.
  • Give each thread its own HermesEnv, or use a pool. Sharing one is safe but slower under contention.

Pooling envs with resource-pool:

import Data.ByteString (ByteString)
import Data.Hermes
import Data.Pool (Pool, defaultPoolConfig, newPool, withResource)

-- Up to 8 envs, each dropped after 60 idle seconds.
mkEnvPool :: IO (Pool HermesEnv)
mkEnvPool = newPool $ defaultPoolConfig mkHermesEnv_ (const $ pure ()) 60 8

decodeWithPool :: Pool HermesEnv -> Decoder a -> ByteString -> IO a
decodeWithPool pool d bs = withResource pool $ \env -> parseByteStringIO env d bs

Limitations

Because the On Demand API in simdjson uses a forward-only iterator (except for object fields), it is possible to introduce unsafe iteration. Hermes tries to prevent this as much as possible with the type system.

The On Demand API does not validate the entire document upon creating the iterator (besides UTF-8 validation and basic well-formed checks). It is possible to parse an invalid JSON document but not realize it until later. If you need the entire document to be validated up front then a DOM parser is a better fit for you.

The On Demand approach is less safe than DOM: we only validate the components of the JSON document that are used and it is possible to begin ingesting an invalid document only to find out later that the document is invalid. Are you fine ingesting a large JSON document that starts with well formed JSON but ends with invalid JSON content?

Other limitations inherited from simdjson:

  • Cannot decode scalar documents, e.g. a single string, number, boolean, or null as a JSON document.
  • 4GB is the maximum document size that simdjson supports.

Portability

Per the simdjson documentation:

A recent compiler (LLVM clang6 or better, GNU GCC 7.4 or better, Xcode 11 or better) on a 64-bit (PPC, ARM or x64 Intel/AMD) POSIX systems such as macOS, freeBSD or Linux. We require that the compiler supports the C++11 standard or better.

However, this library relies on std::string_view without a shim, so C++17 or later is required.

The native_comp cabal flag enables passing -march=native to the C++ compiler.

Passing -march=native to the compiler may make On Demand faster by allowing it to use optimizations specific to your machine. You cannot do this, however, if you are compiling code that might be run on less advanced machines. That is, be mindful that when compiling with the -march=native flag, the resulting binary will run on the current system but may not run on other systems (e.g., on an old processor).

If you are compiling on an ARM or POWER system, you do not need to be concerned with CPU selection during compilation. The -march=native flag is useful for best performance on x64 (e.g., Intel) systems but it is generally unsupported on some platforms such as ARM (aarch64) or POWER.