Getting Started with Stick And String (SAS 1.1) — A Saner Config Format
If you've ever stared at a JSON file wondering why you're missing a comma on line 47, or wrestled with YAML indentation that looks right but somehow isn't — this post is for you.
I built SAS (Stick And String), a human-readable data serialization format designed to be strict, predictable, and free from the quirks that make JSON and YAML frustrating to write by hand. It maps 1:1 with JSON's data model, so you get all the expressiveness without the footguns.
Today I'll walk you through everything you need to know to start using it — the syntax, the libraries, and how to convert your existing JSON files.
Why Another Format?
Quick comparison before we dive in:
JSON — great for machines, annoying for humans. Trailing commas are errors. Comments aren't allowed. Every string needs quotes. One misplaced bracket and the whole file breaks.
YAML — human-friendly on the surface, but indentation-sensitive parsing leads to subtle bugs, and the implicit type coercion (yes becoming true, 1.0 staying a float, etc.) causes real-world headaches.
SAS takes a different approach:
- No commas anywhere
- No indentation rules (whitespace is ignored structurally)
- No implicit type conversion
- Named block closers so you always know what you're closing
- Comments that just work
Here's what a real config file looks like:
# SAS 1.1 config
__sas_version__ -> "1.1"
app ::
name -> "myservice"
version -> "2.4.1"
debug -> false
db ::
host -> "db.internal"
port -> 5432
credentials -> null
:: db
tags -> ["api" | "production" | "v2"]
allowed_hosts ::
- "localhost"
- "127.0.0.1"
- "myservice.internal"
:: allowed_hosts
:: app
Readable, unambiguous, and valid. Let's break down the syntax.
The Syntax in 5 Minutes
Key → Value pairs
Every value is assigned with ->, with exactly one space on each side:
name -> "Alice"
port -> 8080
enabled -> true
ratio -> 3.14
nothing -> null
SAS supports six value types: string, number, boolean, null, object, and array — exactly matching JSON's model.
Strings
Strings use double quotes and support standard escape sequences:
greeting -> "Hello, world!"
path -> "C:\\Users\\dasso"
tab -> "column1\tcolumn2"
emoji -> "\u2728 sparkles"
Need a multiline string? Use triple quotes:
description -> """
This is line one.
This is line two.
Content is preserved exactly as written.
"""
Numbers, Booleans, Null
count -> 42
negative -> -7
pi -> 3.14159
mass -> 1.2e10
active -> true
disabled -> false
value -> null
Note: True, False, NULL etc. are parse errors. SAS is strict — lowercase only.
Block Objects
Objects use named open/close pairs:
server ::
host -> "localhost"
port -> 8080
:: server
The :: server closer must match the opener exactly. This means you always know at a glance what block you're closing — no more hunting for mismatched brackets. Nesting works as deep as you need:
app ::
database ::
primary ::
host -> "db1.internal"
port -> 5432
:: primary
:: database
:: app
Inline Objects
For small flat objects, there's a compact inline form:
origin -> { lat -> 37.77 | lon -> -122.41 }
padding -> { top -> 10 | right -> 20 | bottom -> 10 | left -> 20 }
Fields are separated by | (space, pipe, space). No nesting allowed in inline form.
Arrays
Inline arrays work great for scalars:
tags -> ["api" | "production" | "v2"]
ports -> [8080 | 8443 | 9000]
flags -> [true | false | true]
For longer lists or arrays of objects, use block syntax:
servers ::
- "web1.internal"
- "web2.internal"
- "web3.internal"
:: servers
Arrays of objects use anonymous blocks:
users ::
- ::
name -> "Alice"
role -> "admin"
:: -
- ::
name -> "Bob"
role -> "viewer"
:: -
:: users
Comments
# This is a comment
# Leading whitespace is fine too
name -> "Alice" # THIS IS NOT a comment — inline comments are not allowed
Installation
JavaScript / Node.js
npm install stick-and-string
Python
pip install stick-and-string
Using the JavaScript Library
import { parseSAS, sasToJSON, jsonToSAS } from 'stick-and-string';
// Parse a SAS document into a JS object
const config = parseSAS(`
app ::
name -> "myservice"
port -> 8080
debug -> false
:: app
`);
console.log(config.app.name); // "myservice"
console.log(config.app.port); // 8080
console.log(config.app.debug); // false
Converting to JSON:
import { sasToJSON } from 'stick-and-string';
import { readFileSync } from 'fs';
const json = sasToJSON(readFileSync('config.sas', 'utf8'));
console.log(json);
// {
// "app": {
// "name": "myservice",
// "port": 8080,
// "debug": false
// }
// }
Converting from JSON:
import { jsonToSAS } from 'stick-and-string';
const sas = jsonToSAS({
server: {
host: 'localhost',
port: 8080,
tags: ['api', 'v2']
}
});
console.log(sas);
// __sas_version__ -> "1.1"
//
// server ::
// host -> "localhost"
// port -> 8080
// tags -> ["api" | "v2"]
// :: server
Error handling:
import { parseSAS, SASParseError } from 'stick-and-string';
try {
parseSAS('name -> "Alice"\nname -> "Bob"\n'); // duplicate key
} catch (e) {
if (e instanceof SASParseError) {
console.error(`Parse failed at line ${e.lineNum}: ${e.message}`);
// Parse failed at line 2: [Line 2] E01: Duplicate key "name"
}
}
CommonJS:
const { parseSAS, sasToJSON, jsonToSAS } = require('stick-and-string');
Using the Python Library
from sas_tools import parse_sas, sas_to_json, json_to_sas, SASParseError
# Parse SAS → dict
config = parse_sas(open("config.sas").read())
print(config["app"]["name"]) # "myservice"
print(config["app"]["port"]) # 8080
# SAS → JSON string
json_str = sas_to_json(open("config.sas").read())
# dict → SAS string
sas = json_to_sas({
"server": {
"host": "localhost",
"port": 8080,
"tags": ["api", "v2"]
}
})
print(sas)
Error handling:
from sas_tools import parse_sas, SASParseError
try:
parse_sas('name -> "Alice"\nname -> "Bob"\n')
except SASParseError as e:
print(f"Line {e.line_num}: {e}")
# Line 2: [Line 2] E01: Duplicate key "name"
The CLI
Both packages install a sas command for use in the terminal or CI pipelines.
Validate a file:
sas validate config.sas
# ✓ Valid SAS 1.1 document: config.sas
Convert SAS → JSON:
sas to-json config.sas
sas to-json config.sas --output config.json
sas to-json config.sas --compact # minified JSON
Convert JSON → SAS:
sas to-sas data.json
sas to-sas data.json --output data.sas
Round-trip check (great for CI):
sas roundtrip config.sas
# Roundtrip: config.sas (SAS → JSON → SAS)
#
# ✓ Parse SAS
# ✓ Re-encode to SAS and re-parse
# ✓ Data preserved exactly
#
# ✓ Roundtrip OK
Migrating an Existing JSON File
Got an existing config.json you want to move to SAS? One command:
sas to-sas config.json --output config.sas
The converter handles everything automatically — nested objects become block objects, scalar arrays become inline arrays, large arrays of objects become block arrays with anonymous elements.
Error Codes
SAS parsers never silently skip errors. Every problem comes with a line number and a code:
| Code | What it means |
|---|---|
| E01 | Duplicate key in same scope |
| E02 | Block closer doesn't match opener |
| E03 | Unclosed block at end of file |
| E04 | Invalid escape sequence in string |
| E05 | Invalid number (leading zero, NaN, etc.) |
| E06 | Wrong-case boolean or null (True, NULL) |
| E07 | Inline comment |
| E08 | Missing spaces around ->
|
| E09 | Missing spaces around `\ |
| E10 | Trailing {% raw %}`\ |
| E11 | Non-scalar value in inline array |
| E12 | Nested inline object |
| E13 | Key beginning with {% raw %}-
|
| E14 | Mixed block content (pairs + array items) |
| E15 | Anonymous closer outside array context |
What's Next
The spec, full API docs, and source code are all on GitHub. Both implementations are fully tested — 69 tests for JavaScript, 68 for Python — and the format spec is available as a readable Markdown document in the repo if you want to build your own parser in another language.
- GitHub: https://github.com/TheServer-lab/stick-and-string
- npm: https://www.npmjs.com/package/stick-and-string
- PyPI: https://pypi.org/project/stick-and-string/
Give it a try and let me know what you think in the comments. And if you do build a parser in another language, I'd love to know!
Top comments (0)