Skip to content
haus haus

A data format that feels like home.

Clean to write, with real types built in: variants, dates and times, exact numbers. And your data can grow without breaking the code that reads it.

Draft 0.1. Still changing, so don't use it in production yet.

The haus mark: a little house with smoke rising from its chimney

This is haus.

An order, written the way you'd describe it. If you can read JSON, you can read this.

order.haus
// Comments are welcome, keys don't need quotes.
{
  order: 1042,
  placed: Date("2026-11-08"),
  total: 30.30,
  status: Shipped {
    tracking: "DE-4711",
    at: Instant("2026-11-09T09:12:00Z"),
  },
  gift: True,
  note: None,
}
  • Shipped is a variant. The status names its case and carries the data that goes with it.
  • Date is a real date, checked by every parser. No guessing what a string means.
  • 30.30 stays 30.30. Your code gets exactly what you wrote.
  • None means no value. Like True, it's a plain tag, so there's no null.

What makes it a home.

What every project ends up inventing for itself, built into the format once.

At home in your code.

Any haus file parses on its own, just like JSON. When you want types, put a .schema.haus file next to your data: your build reads it and gives you real types. Today in TypeScript, Rust and Roc, with more languages to come.

thread.haus
Comment {
  author: "ada",
  at: Instant("2026-11-01T09:00:00Z"),
  text: "Should haus have null?",
  likes: 0,
  replies: [
    Comment {
      author: "bob",
      at: Instant("2026-11-02T09:00:00Z"),
      text: "JSON has it, people expect it.",
      likes: 2,
      replies: [],
    },
  ],
}

A comment thread, where each reply is a comment too.

thread.schema.haus
import std/time (Instant)

type Comment = closed < Comment {
  author: String,
  at: Instant,
  text: String,
  likes: Int,
  replies: Array Comment,
} >

type Root = Comment

Its schema. Comment refers to itself, so threads can nest.

main.ts
import * as Haus from "@haus-fmt/effect"
import { Effect } from "effect"
import thread from "./thread.haus"     // checked against its schema
import { Comment } from "./thread.schema.haus"

thread.author      // String        → string
thread.at          // Instant       → DateTime.Utc
thread.likes       // Int           → bigint, no size limit
thread.replies     // Array Comment → Comment[]

const program = Effect.gen(function* () {
  const liked = { ...thread, likes: thread.likes + 1n }
  return yield* Haus.encode(Comment, { style: "compact" })(liked)
})

Using the Effect binding. Zod, Valibot and ArkType work too, through Standard Schema.

main.rs
mod thread {
    haus::include_schema!("thread"); // from thread.schema.haus
}
use thread::Comment;

let thread: Comment = haus::from_str(include_str!("thread.haus"))?;

thread.author      // String        → String
thread.likes       // Int           → haus::Int, no size limit
thread.replies     // Array Comment → Vec<Comment>

let likes = thread.likes.to_i128().unwrap_or_default() + 1;
let liked = Comment { likes: likes.into(), ..thread };
println!("{}", haus::to_string(&liked));

haus-build generates the types at compile time.

main.roc
import haus.Decode
import haus.Encode
import Thread

path : Path
path = "thread.haus"
thread = Decode.one(path.read_utf8!()?, Thread.decode_root)?

# author  : Str            String
# likes   : I128           Int
# replies : List(Comment)  Array Comment

liked = match thread {
    Comment(c) => Comment({ ..c, likes: c.likes + 1 })
}
Stdout.line!(Encode.to_str(Thread.encode_comment(liked)))?

haus-gen generates the types. The Roc parser is still in progress.

The same data in JSON.

JSON can hold all of it, but only by convention: a "type" field for the status, plain strings for the date and the amount. Every team has to agree on those rules and write them down. In haus, they're part of the format.

order.haus
{
  order: 1042,
  placed: Date("2026-11-08"),
  total: 30.30,
  status: Shipped {
    tracking: "DE-4711",
    at: Instant("2026-11-09T09:12:00Z"),
  },
  gift: True,
  note: None,
}
order.json
{
  "order": 1042,
  "placed": "2026-11-08",
  "total": "30.30",
  "status": {
    "type": "shipped",
    "tracking": "DE-4711",
    "at": "2026-11-09T09:12:00Z"
  },
  "gift": true,
  "note": null
}
The haus mark: a little house with smoke rising from its chimney