<rss version="2.0">
  <channel>
    <title>Dhall on LLBBL Blog</title>
    <link>https://llbbl.blog/categories/dhall/</link>
    <description></description>
    
    <language>en</language>
    
    <lastBuildDate>Sat, 11 Jul 2026 10:00:00 -0500</lastBuildDate>
    
    <item>
      <title>Adding Types to JSON with Dhall</title>
      <link>https://llbbl.blog/2026/07/11/adding-types-to-json-with.html</link>
      <pubDate>Sat, 11 Jul 2026 10:00:00 -0500</pubDate>
      
      <guid>http://llbbl.micro.blog/2026/07/11/adding-types-to-json-with.html</guid>
      <description>&lt;p&gt;A few months ago I wrote a post asking &lt;a href=&#34;https://llbbl.blog/2026/03/29/is-there-something-better-than.html&#34;&gt;whether there&amp;rsquo;s something better than JSON&lt;/a&gt;. Two configuration languages that sit &lt;em&gt;above&lt;/em&gt; 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.&lt;/p&gt;
&lt;p&gt;JSON is the universal language of API payloads and config files, and I don&amp;rsquo;t want that to change. But as a format for &lt;em&gt;authoring&lt;/em&gt; configuration by hand, it&amp;rsquo;s rough:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No comments.&lt;/li&gt;
&lt;li&gt;No variables or functions, so you copy-paste the same block ten times.&lt;/li&gt;
&lt;li&gt;No type system, so &lt;code&gt;&amp;quot;8080&amp;quot;&lt;/code&gt; and &lt;code&gt;8080&lt;/code&gt; look equally valid.&lt;/li&gt;
&lt;li&gt;No imports, which is how you end up with a 2,000-line monolith nobody wants to touch.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;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&amp;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&amp;rsquo;ll do it at 2 AM when the pipeline runs.&lt;/p&gt;
&lt;p&gt;This is where &lt;a href=&#34;https://dhall-lang.org&#34;&gt;Dhall&lt;/a&gt; comes in.&lt;/p&gt;
&lt;h2 id=&#34;what-is-dhall&#34;&gt;What is Dhall?&lt;/h2&gt;
&lt;p&gt;The short version: Dhall is JSON plus types, plus functions, plus imports. It&amp;rsquo;s a programmable, strongly-typed configuration language.&lt;/p&gt;
&lt;p&gt;The part I actually care about is what it &lt;em&gt;doesn&amp;rsquo;t&lt;/em&gt; have. Dhall is &lt;strong&gt;not Turing-complete&lt;/strong&gt;. 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&amp;rsquo;s a different trade than &amp;ldquo;just write a Python script.&amp;rdquo;&lt;/p&gt;
&lt;h2 id=&#34;the-problem-in-json&#34;&gt;The problem, in JSON&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s a normal &lt;code&gt;config.json&lt;/code&gt; for a microservice:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;{
  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;serviceName&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;payment-api&amp;#34;&lt;/span&gt;,
  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;port&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;8080&lt;/span&gt;,
  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;environment&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;production&amp;#34;&lt;/span&gt;,
  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;database&amp;#34;&lt;/span&gt;: {
    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;host&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;db.internal.net&amp;#34;&lt;/span&gt;,
    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;maxConnections&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;50&lt;/span&gt;
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Three ways can be a problem in production: someone writes &lt;code&gt;&amp;quot;port&amp;quot;: &amp;quot;8080&amp;quot;&lt;/code&gt; and the service won&amp;rsquo;t boot, someone typos &lt;code&gt;&amp;quot;prodution&amp;quot;&lt;/code&gt; and it silently runs in debug mode, or someone forgets &lt;code&gt;maxConnections&lt;/code&gt; entirely and you get a null blowup at runtime. Nothing catches any of it until it&amp;rsquo;s live.&lt;/p&gt;
&lt;h2 id=&#34;the-same-thing-typed&#34;&gt;The same thing, typed&lt;/h2&gt;
&lt;p&gt;In Dhall you define the shape up front. Enums, record types, default values:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-dhall&#34; data-lang=&#34;dhall&#34;&gt;-- schema.dhall
let Environment = &amp;lt; Local | Staging | Production &amp;gt;

let Database =
      { Type = { host : Text, maxConnections : Natural }
      , default = { maxConnections = 20 }
      }

let Config =
      { Type =
          { serviceName : Text
          , port : Natural
          , environment : Environment
          , database : Database.Type
          }
      , default =
          { port = 8080, environment = Environment.Local }
      }

in  { Environment, Database, Config }
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now you author against that schema, and you get defaults and composition for free:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-dhall&#34; data-lang=&#34;dhall&#34;&gt;-- config.dhall
let Schema = ./schema.dhall

let myConfig : Schema.Config.Type =
      Schema.Config.default
      // { serviceName = &amp;quot;payment-api&amp;quot;
         , environment = Schema.Environment.Production
         , database = Schema.Database.default // { host = &amp;quot;db.internal.net&amp;quot;, maxConnections = 50 }
         }

in  myConfig
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Misspell &lt;code&gt;Production&lt;/code&gt;, or pass &lt;code&gt;&amp;quot;8080&amp;quot;&lt;/code&gt; as a string, and Dhall throws a type error &lt;em&gt;before a single line of JSON is generated&lt;/em&gt;. Hopefully the benfit is now clear; adding a type safety layer to your config files.&lt;/p&gt;
&lt;h2 id=&#34;compiling-down-to-json&#34;&gt;Compiling down to JSON&lt;/h2&gt;
&lt;p&gt;You don&amp;rsquo;t ship Dhall to your services. You ship the JSON they already understand:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;brew install dhall-json
dhall-to-json --file config.dhall
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;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.&lt;/p&gt;
&lt;h2 id=&#34;two-features-worth-knowing-about&#34;&gt;Two features worth knowing about&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Hermetic imports with hash pinning.&lt;/strong&gt; 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:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-dhall&#34; data-lang=&#34;dhall&#34;&gt;let Prelude =
      https://prelude.dhall-lang.org/v22.0.0/package.dhall
        sha256:10db4c919c25e4d262db3ed0d1d6120da3e3906673f00e3012c1d14e1963976a
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If the remote content changes, the hash won&amp;rsquo;t match and the build fails. The hash above is just an example, and each Prelude version has its own, so don&amp;rsquo;t copy it by hand. &lt;code&gt;dhall freeze --inplace config.dhall&lt;/code&gt; computes the correct hashes for whatever you&amp;rsquo;ve imported and pins them automatically.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Exhaustive matching with &lt;code&gt;merge&lt;/code&gt;.&lt;/strong&gt; When you map a union type to output, Dhall makes you handle every variant:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-dhall&#34; data-lang=&#34;dhall&#34;&gt;let getLogPrefix = \(env : Environment) -&amp;gt;
      merge
        { Local = &amp;quot;[DEV] &amp;quot;, Staging = &amp;quot;[STAGE] &amp;quot;, Production = &amp;quot;[PROD] &amp;quot; }
        env
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Add a &lt;code&gt;QA&lt;/code&gt; variant later, and every &lt;code&gt;merge&lt;/code&gt; block that touched &lt;code&gt;Environment&lt;/code&gt; fails to compile until you deal with it. No forgotten &lt;code&gt;switch&lt;/code&gt; case slipping into production. The compiler keeps a running list of everything you now owe it.&lt;/p&gt;
&lt;h2 id=&#34;is-it-worth-it&#34;&gt;Is it worth it?&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Raw JSON&lt;/th&gt;
&lt;th&gt;Dhall&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Type safety&lt;/td&gt;
&lt;td&gt;None, fails at runtime&lt;/td&gt;
&lt;td&gt;Static, at compile time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comments &amp;amp; logic&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Termination&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Guaranteed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency pinning&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;SHA-256&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;Consumed directly&lt;/td&gt;
&lt;td&gt;Compiles to JSON/YAML/TOML&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For a two-key config file, it doesn&amp;rsquo;t make sense, but once you&amp;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.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;sources&#34;&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://docs.dhall-lang.org/&#34;&gt;Dhall Language Tutorial &amp;amp; Cheatsheet&lt;/a&gt;: records, union types, default overrides, &lt;code&gt;dhall-to-json&lt;/code&gt;, and &lt;code&gt;dhall freeze&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/dhall-lang/dhall-lang&#34;&gt;Dhall language standard on GitHub&lt;/a&gt;: the non-Turing-complete design and the semantic integrity hash spec.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;I&amp;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 &lt;a href=&#34;https://micro.blog/llbbl?remote_follow=1&#34;&gt;@logan@llbbl.blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
  </channel>
</rss>