go get github.com/pt-main/tyclTYCL is a typed configuration language for Go.
It provides strong typing, contracts (schemas), and a readable syntax — with no code generation and no interface{}.
| Format | Problems | TYCL solves |
|---|---|---|
| JSON | no comments, everything via interface{}, no validation |
strong typing, comments, contracts |
| YAML | whitespace‑sensitive, no typing | explicit types, deterministic parsing |
| TOML | limited, no schemas | contracts, flexible structures |
TYCL gives you 70% of the power of complex config languages at 30% of the complexity.
It works both as a primary configuration format and as an intermediate representation — you can write TYCL and then generate JSON, YAML, or TOML for integration with other systems.
go get github.com/pt-main/tyclUse it in your code:
import "github.com/pt-main/tycl"
cfg, err := tycl.Process(`{ port: int = 8080 }`, `strict { port: int }`)
if err != nil {
log.Fatal(err)
}
port := cfg.IntV["port"] // 8080Download a binary from the releases page, or install via go install:
go install github.com/pt-main/tycl/tycl@latestCommands:
tycl valid <config> [contract]— validate a config against a contract.tycl syntax <file...>— check syntax and types (without a contract).tycl fmt <type> <file...>— format (conf/contract).tycl gen <input> <output> <json|yaml|toml>— generate a target format.tycl contract <input> <output> <type>— generate a contract from a config (strict,flexible,dynamic).
TYCL is close to JSON, but every field has an explicit type.
port: int = 8080
rate: float = 1.5
debug: bool = true
host: string = "localhost"
TYCL allows you to specify that a field exists but its value is absent, while the field type is known:
timeout: int = null /* field timeout exists but is null, type int */
Rules for null:
- Type is mandatory —
nullalways comes with a type (int,string, etc.). - Uniqueness of null — there can be only one null value for a given name.
If you declaretimeout: int = null, you cannot addtimeout: string = null, but you can addtimeout: string = "5s"(not null). - Null ≠ missing field —
key: int = nullmeans the field exists but its value is not set. If the field is not mentioned in the config at all, it is simply absent (and the contract will notice that).
Arrays are strictly typed and denoted by the type in plural form. The array type is mandatory:
ports: ints = [8080, 8081]
names: strings = ["dev", "prod"]
rates: floats = [1.1, 2.2]
flags: bools = [true, false]
servers: objects = [
{ host: string = "a", port: int = 80 },
{ host: string = "b", port: int = 443 }
]
All elements of an array must have the same type.
server: object = {
host: string = "127.0.0.1"
port: int = 8080
timeout: int = null
}
Objects can be nested arbitrarily:
app: object = {
name: string = "myapp"
database: object = {
host: string = "localhost"
port: int = 5432
}
}
Only block comments /* ... */ are supported, and they may be placed only at the beginning or at the end of a block (object or array). This makes comments act as documentation for the block.
/* This object describes the server */
server: object = {
host: string = "127.0.0.1"
port: int = 8080
} /* End of server description */
Line comments (//) are not supported.
-
Types in keys are optional
If the type is omitted, it is inferred from the value:
key = "text"→string,key = 42→int.
Exception: for arrays the type is mandatory (ints,strings, etc.). -
Duplicate keys with different types
Allowed, but not recommended, because when generating JSON/YAML/TOML, a conflict will cause an error.
Example:port: int = 8080 port: string = "8080" /* allowed, but bad practice */ -
Null
There can be only one key with a given name if it equalsnull(regardless of type).timeout: int = null /* ok */ timeout: string = null /* error: null already exists for timeout */ timeout: string = "5s" /* ok, this is not null */
The tycl.Process function returns a *Config object that contains separate maps for each data type. This lets you access values without type assertions:
type Config struct {
IntV map[string]int
FloatV map[string]float64
BoolV map[string]bool
StringV map[string]string
NullV map[string]string // key → type of null value
IntArrV map[string][]int
FloatArrV map[string][]float64
BoolArrV map[string][]bool
StringArrV map[string][]string
InnerV map[string]*Config // objects
InnerArrV map[string][]*Config // arrays of objects
}Example access:
cfg, _ := tycl.Process(`{ port: int = 8080, host: string = "localhost" }`, "")
port := cfg.IntV["port"] // 8080 (int)
host := cfg.StringV["host"] // "localhost" (string)The generation package allows exporting a *Config to JSON, YAML, TOML, and back to TYCL:
import "github.com/pt-main/tycl/generation"
jsonStr, err := generation.Json(cfg)
yamlStr, err := generation.Yaml(cfg)
tomlStr, err := generation.Toml(cfg)
tyclStr, err := generation.Tycl(cfg) // back to TYCLThis is useful when you load a config, modify it in code, and want to save it in another format.
A contract describes the expected structure of a config. It is written in the same language, but instead of values, only types are given.
strict {
port: int
host: string
debug: bool
timeout: int
ports: ints
server: object = strict {
host: string
port: int
}
}
Strictness levels:
dynamic— no validation (any structure allowed).flexible— all listed fields must be present, extra fields are allowed.strict— exact match (no extra fields allowed).
Contracts support nesting for objects and arrays of objects:
test1: objects = flexible {
key: string
}
This means that every element of the test1 array must be an object with a key field of type string.
The CLI tool allows exporting a config to JSON, YAML, or TOML without writing any code:
tycl gen config.tycl out.json json
tycl gen config.tycl out.yaml yaml
tycl gen config.tycl out.toml tomlThis turns TYCL into an intermediate language: you write safe and readable TYCL, then generate files for integration with other systems.
TYCL can automatically generate contracts from existing configs:
tycl contract config.tycl contract.tycl strictThis is useful when you already have a config and want to create a schema for validating future changes.
package main
import (
"fmt"
"log"
"github.com/pt-main/tycl"
"github.com/pt-main/tycl/generation"
)
func main() {
conf := `{
port: int = 8080
host: string = "localhost"
timeout: int = null
test1: objects = [
{ key: string = "a" },
{ key: string = "b" }
]
}`
contract := `strict {
port: int
host: string
timeout: int
test1: objects = flexible {
key: string
}
}`
cfg, err := tycl.Process(conf, contract)
if err != nil {
log.Fatal(err)
}
fmt.Println(cfg.IntV["port"]) // 8080
fmt.Println(cfg.StringV["host"]) // "localhost"
// Export to JSON
jsonData, _ := generation.Json(cfg)
fmt.Println(jsonData)
// Generate a contract from the config
cont, _ := generation.ContractFromConfig(cfg, shared.ContractStrict)
contCode, _ := generation.GenerateContractCode(cont)
fmt.Println(contCode)
}If you do not need a contract, pass "" or "dynamic{}" — validation will be skipped.
- VS Code plugin: syntax highlighting, autocompletion, formatting, live contract checking.
Apache 2.0 — see LICENSE for details.
TYCL — built for convenience, safety, and simplicity. Try it — and you won't want to go back to JSON.