{
  "version": "https://jsonfeed.org/version/1",
  "title": "Dhall on LLBBL Blog",
  "icon": "https://avatars.micro.blog/avatars/2023/40/125738.jpg",
  "home_page_url": "https://llbbl.blog/",
  "feed_url": "https://llbbl.blog/feed.json",
  "items": [
      {
        "id": "http://llbbl.micro.blog/2026/07/11/adding-types-to-json-with.html",
        "title": "Adding Types to JSON with Dhall",
        "content_html": "<p>A few months ago I wrote a post asking <a href=\"https://llbbl.blog/2026/03/29/is-there-something-better-than.html\">whether there&rsquo;s something better than JSON</a>. Two configuration languages that sit <em>above</em> JSON kept coming up: CUE and Dhall. Both give you the things JSON lacks when you author config by hand, and both compile down to plain JSON, YAML, or whatever your services actually read. I spent more time with CUE back then and never gave Dhall a real look. This post is me going back for that second look, because the one feature I kept wanting was a type system over my config.</p>\n<p>JSON is the universal language of API payloads and config files, and I don&rsquo;t want that to change. But as a format for <em>authoring</em> configuration by hand, it&rsquo;s rough:</p>\n<ul>\n<li>No comments.</li>\n<li>No variables or functions, so you copy-paste the same block ten times.</li>\n<li>No type system, so <code>&quot;8080&quot;</code> and <code>8080</code> look equally valid.</li>\n<li>No imports, which is how you end up with a 2,000-line monolith nobody wants to touch.</li>\n</ul>\n<p>The usual escape hatch is a templating engine like Jinja or Helm, or a real programming language like Python or TypeScript that spits out JSON. That works, but you&rsquo;ve traded one problem for a scarier one: your config generator is now Turing-complete. It can crash, hang in an infinite loop, or reach out and read some local environment variable, and it&rsquo;ll do it at 2 AM when the pipeline runs.</p>\n<p>This is where <a href=\"https://dhall-lang.org\">Dhall</a> comes in.</p>\n<h2 id=\"what-is-dhall\">What is Dhall?</h2>\n<p>The short version: Dhall is JSON plus types, plus functions, plus imports. It&rsquo;s a programmable, strongly-typed configuration language.</p>\n<p>The part I actually care about is what it <em>doesn&rsquo;t</em> have. Dhall is <strong>not Turing-complete</strong>. No arbitrary recursion, no side effects. Every Dhall program is guaranteed to terminate. You get the abstraction power of a functional language like Haskell or Elm, with the guarantee that it will never hang your build. That&rsquo;s a different trade than &ldquo;just write a Python script.&rdquo;</p>\n<h2 id=\"the-problem-in-json\">The problem, in JSON</h2>\n<p>Here&rsquo;s a normal <code>config.json</code> for a microservice:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-json\" data-lang=\"json\">{\n  <span style=\"color:#f92672\">&#34;serviceName&#34;</span>: <span style=\"color:#e6db74\">&#34;payment-api&#34;</span>,\n  <span style=\"color:#f92672\">&#34;port&#34;</span>: <span style=\"color:#ae81ff\">8080</span>,\n  <span style=\"color:#f92672\">&#34;environment&#34;</span>: <span style=\"color:#e6db74\">&#34;production&#34;</span>,\n  <span style=\"color:#f92672\">&#34;database&#34;</span>: {\n    <span style=\"color:#f92672\">&#34;host&#34;</span>: <span style=\"color:#e6db74\">&#34;db.internal.net&#34;</span>,\n    <span style=\"color:#f92672\">&#34;maxConnections&#34;</span>: <span style=\"color:#ae81ff\">50</span>\n  }\n}\n</code></pre></div><p>Three ways can be a problem in production: someone writes <code>&quot;port&quot;: &quot;8080&quot;</code> and the service won&rsquo;t boot, someone typos <code>&quot;prodution&quot;</code> and it silently runs in debug mode, or someone forgets <code>maxConnections</code> entirely and you get a null blowup at runtime. Nothing catches any of it until it&rsquo;s live.</p>\n<h2 id=\"the-same-thing-typed\">The same thing, typed</h2>\n<p>In Dhall you define the shape up front. Enums, record types, default values:</p>\n<pre tabindex=\"0\"><code class=\"language-dhall\" data-lang=\"dhall\">-- schema.dhall\nlet Environment = &lt; Local | Staging | Production &gt;\n\nlet Database =\n      { Type = { host : Text, maxConnections : Natural }\n      , default = { maxConnections = 20 }\n      }\n\nlet Config =\n      { Type =\n          { serviceName : Text\n          , port : Natural\n          , environment : Environment\n          , database : Database.Type\n          }\n      , default =\n          { port = 8080, environment = Environment.Local }\n      }\n\nin  { Environment, Database, Config }\n</code></pre><p>Now you author against that schema, and you get defaults and composition for free:</p>\n<pre tabindex=\"0\"><code class=\"language-dhall\" data-lang=\"dhall\">-- config.dhall\nlet Schema = ./schema.dhall\n\nlet myConfig : Schema.Config.Type =\n      Schema.Config.default\n      // { serviceName = &quot;payment-api&quot;\n         , environment = Schema.Environment.Production\n         , database = Schema.Database.default // { host = &quot;db.internal.net&quot;, maxConnections = 50 }\n         }\n\nin  myConfig\n</code></pre><p>Misspell <code>Production</code>, or pass <code>&quot;8080&quot;</code> as a string, and Dhall throws a type error <em>before a single line of JSON is generated</em>. Hopefully the benfit is now clear; adding a type safety layer to your config files.</p>\n<h2 id=\"compiling-down-to-json\">Compiling down to JSON</h2>\n<p>You don&rsquo;t ship Dhall to your services. You ship the JSON they already understand:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-bash\" data-lang=\"bash\">brew install dhall-json\ndhall-to-json --file config.dhall\n</code></pre></div><p>Out comes clean, boring, standard JSON. Your services never know Dhall was involved. The part that I like is the safety lives at authoring time, and the runtime artifact stays dumb.</p>\n<h2 id=\"two-features-worth-knowing-about\">Two features worth knowing about</h2>\n<p><strong>Hermetic imports with hash pinning.</strong> Dhall can import from a URL, so shared utilities live in one place instead of being copy-pasted across five repos. To keep someone from swapping the file out from under you, you pin the import to a SHA-256 hash of its normalized form:</p>\n<pre tabindex=\"0\"><code class=\"language-dhall\" data-lang=\"dhall\">let Prelude =\n      https://prelude.dhall-lang.org/v22.0.0/package.dhall\n        sha256:10db4c919c25e4d262db3ed0d1d6120da3e3906673f00e3012c1d14e1963976a\n</code></pre><p>If the remote content changes, the hash won&rsquo;t match and the build fails. The hash above is just an example, and each Prelude version has its own, so don&rsquo;t copy it by hand. <code>dhall freeze --inplace config.dhall</code> computes the correct hashes for whatever you&rsquo;ve imported and pins them automatically.</p>\n<p><strong>Exhaustive matching with <code>merge</code>.</strong> When you map a union type to output, Dhall makes you handle every variant:</p>\n<pre tabindex=\"0\"><code class=\"language-dhall\" data-lang=\"dhall\">let getLogPrefix = \\(env : Environment) -&gt;\n      merge\n        { Local = &quot;[DEV] &quot;, Staging = &quot;[STAGE] &quot;, Production = &quot;[PROD] &quot; }\n        env\n</code></pre><p>Add a <code>QA</code> variant later, and every <code>merge</code> block that touched <code>Environment</code> fails to compile until you deal with it. No forgotten <code>switch</code> case slipping into production. The compiler keeps a running list of everything you now owe it.</p>\n<h2 id=\"is-it-worth-it\">Is it worth it?</h2>\n<table>\n<thead>\n<tr>\n<th></th>\n<th>Raw JSON</th>\n<th>Dhall</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Type safety</td>\n<td>None, fails at runtime</td>\n<td>Static, at compile time</td>\n</tr>\n<tr>\n<td>Comments &amp; logic</td>\n<td>No</td>\n<td>Yes</td>\n</tr>\n<tr>\n<td>Termination</td>\n<td>N/A</td>\n<td>Guaranteed</td>\n</tr>\n<tr>\n<td>Dependency pinning</td>\n<td>No</td>\n<td>SHA-256</td>\n</tr>\n<tr>\n<td>Output</td>\n<td>Consumed directly</td>\n<td>Compiles to JSON/YAML/TOML</td>\n</tr>\n</tbody>\n</table>\n<p>For a two-key config file, it doesn&rsquo;t make sense, but once you&rsquo;re staring down Kubernetes manifests, a pile of near-identical microservice configs, or anything where a typo takes down a service, the calculus changes. You keep clean static JSON as the thing your services actually read, and you move all the ways-to-get-it-wrong to a place where a compiler catches them first.</p>\n<hr>\n<h2 id=\"sources\">Sources</h2>\n<ul>\n<li><a href=\"https://docs.dhall-lang.org/\">Dhall Language Tutorial &amp; Cheatsheet</a>: records, union types, default overrides, <code>dhall-to-json</code>, and <code>dhall freeze</code>.</li>\n<li><a href=\"https://github.com/dhall-lang/dhall-lang\">Dhall language standard on GitHub</a>: the non-Turing-complete design and the semantic integrity hash spec.</li>\n</ul>\n<hr>\n<blockquote>\n<p>I&rsquo;d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at <a href=\"https://micro.blog/llbbl?remote_follow=1\">@logan@llbbl.blog</a>.</p>\n</blockquote>\n",
        "date_published": "2026-07-11T10:00:00-05:00",
        "url": "https://llbbl.blog/2026/07/11/adding-types-to-json-with.html",
        "tags": ["DevOps","Programming","Json","Configuration","Dhall"]
      }
  ]
}
