DEV Community

Server-lab
Server-lab

Posted on

Getting Started with Stick And String (SAS 1.1) — A Saner Config Format

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Need a multiline string? Use triple quotes:

description -> """
This is line one.
This is line two.
Content is preserved exactly as written.
"""
Enter fullscreen mode Exit fullscreen mode

Numbers, Booleans, Null

count    -> 42
negative -> -7
pi       -> 3.14159
mass     -> 1.2e10
active   -> true
disabled -> false
value    -> null
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

For longer lists or arrays of objects, use block syntax:

servers ::
    - "web1.internal"
    - "web2.internal"
    - "web3.internal"
:: servers
Enter fullscreen mode Exit fullscreen mode

Arrays of objects use anonymous blocks:

users ::
    - ::
        name -> "Alice"
        role -> "admin"
    :: -
    - ::
        name -> "Bob"
        role -> "viewer"
    :: -
:: users
Enter fullscreen mode Exit fullscreen mode

Comments

# This is a comment
    # Leading whitespace is fine too

name -> "Alice"  # THIS IS NOT a comment — inline comments are not allowed
Enter fullscreen mode Exit fullscreen mode

Installation

JavaScript / Node.js

npm install stick-and-string
Enter fullscreen mode Exit fullscreen mode

Python

pip install stick-and-string
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
//   }
// }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

CommonJS:

const { parseSAS, sasToJSON, jsonToSAS } = require('stick-and-string');
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Convert SAS → JSON:

sas to-json config.sas
sas to-json config.sas --output config.json
sas to-json config.sas --compact   # minified JSON
Enter fullscreen mode Exit fullscreen mode

Convert JSON → SAS:

sas to-sas data.json
sas to-sas data.json --output data.sas
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

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)