Most tools today ship YAML or TOML configuration with no schema, no validation, and no LSP, leaving users to guess structure and discover errors only in production. Misconfigurations lead to operational outages and security incidents.
Good configuration DX needs:
C4 provides all of this by letting you define configuration intent explicitly in Go β then generating everything else from that definition.
You declare schema using a builder API (similar in spirit to Cobra flags):
func (c *Server) ConfigSchema(b *c4.SchemaBuilder) {
b.Field(&c.Port).
Default(8080).
GreaterThan(0).
LessThan(65535)
}From this one definition, C4 generates:
- a schema AST
- CEL validation rules
- defaults & examples
- Markdown documentation
- a full LSP (hover, completion, diagnostics)
Define intent once; C4 handles the rest.
Most config failures are preventable if the schema, rules, and examples are defined early, close to the code. C4 makes this trivial: once you declare fields and rules, it automatically produces validation, documentation, examples, and LSP diagnostics.
This also makes configuration agent-friendly β coding agents and MCP servers can auto-fill or fix configs because the schema is machine-readable.
And because docs and validation come directly from your schema, you canβt end up shipping a config surface without shipping its docs.
Great at loading YAML/TOML β structs(i.e schema-on-read), but they donβt provide:
- schema & validation
- generate config documentation
- LSP support
When we expose configuration to users, it deserves the same level of intent and structure that we apply when designing a database schema.
They solve two things:
- Structure
- Validation
But they require:
- learning a separate language
- maintaining schema outside Go
- keeping two sources of truth in sync
Go structs already define structure. C4 adds rules, docs, and examples through an explicit builder API β no DSLs, no drift, no duplication.
- Everything stays in Go
- Schema is explicit, readable, and greppable
- Supports human-friendly data formats
- Generates docs & LSP tooling automatically
- No struct tags, no DSLs, no scaffolding
- Perfect balance of control and convenience
CEL fits perfectly as the validation engine because it is:
- simple
- familiar (common in infra tooling & growing)
- safe (sandboxed, no user-defined Go funcs)
- precise
- easy for users to read and reason about
- works across data formats
- works at runtime without generating code
C4βs validation layer is directly inspired by protovalidate from Buf β a beautifully clean, declarative approach where:
- CEL is used for custom rules
- the surrounding framework defines sensible built-ins
- validation is entirely data-driven
C4 brings the same cleanliness to configuration that protovalidate brought to APIs.
C4 currently supports:
- YAML
- TOML
This keeps adoption smooth: users work in formats they already understand.
C4βs schema and validation layers are fully format-agnostic, meaning:
- additional formats could be added in the future
- nothing in the core is tied to YAML/TOML parsing
- but we intentionally stay focused on the two most common human-facing formats for now
C4 uses hand-crafted parsers instead of Tree-Sitter because, as a library, we wanted to avoid the CGo dependency. Tree-Sitter is a powerful tool, but its C core adds complexity to distribution and cross-compilation. Our custom parsers are lightweight, focused, and entirely in Go β making C4 trivial to embed and ship as a library dependency.
C4 intentionally avoids struct tags because:
(e.g., conditional logic, CEL expressions, examples, descriptions)
(e.g., yaml:"..."), which makes them a poor place to define schema semantics
Cobra defines flags explicitly through builder APIs β not tags. C4 applies the same principle to configuration fields.
Explicit > implicit. Clear > clever. Format-agnostic > format-tied.
See working examples in the examples/ directory:
basic/β Simple config struct with validation and marshalinglsp/β Full LSP server with hover, completion, and diagnosticsnested/β Nested configuration structurestoml-config/β TOML format example
This guide walks you through the full workflow: creating a configuration struct, unmarshaling config files, generating documentation, and hooking up an LSP server for editor support.
Create a struct with yaml, json, and toml tags (for format flexibility):
package main
import "github.com/realharshthakur/c4"
type AppConfig struct {
Name string `yaml:"name" json:"name" toml:"name"`
Port int `yaml:"port" json:"port" toml:"port"`
Host string `yaml:"host" json:"host" toml:"host"`
Debug bool `yaml:"debug" json:"debug" toml:"debug"`
}Add a ConfigSchema method to your struct. This is where you define rules, defaults, and constraints using the fluent builder API:
func (c *AppConfig) ConfigSchema(b *c4.SchemaBuilder) {
b.Description("Application server configuration")
b.Field(&c.Name).
Required().
MinLength(1).
MaxLength(50).
Description("Application name")
b.Field(&c.Port).
Default(8080).
GreaterThan(0).
LessThan(65535).
Description("Port number to listen on")
b.Field(&c.Host).
Default("localhost").
Description("Host address to bind to")
b.Field(&c.Debug).
Default(false).
Description("Enable debug mode for verbose logging")
}Builder API methods:
.Required()β Field must be set (non-zero value).Default(value)β Default value if not provided.Description(text)β Documentation text.GreaterThan(n)/.LessThan(n)β Numeric bounds.MinLength(n)/.MaxLength(n)β String/slice length.Pattern(regex)β Regex validation for strings.Enum(values...)β Restrict to allowed values.ValidationRule(expr, message)β Custom CEL validation.RequiredIf(celExpr)β Conditional requirement
c4.Unmarshal() automatically:
- Detects the format (YAML, JSON, or TOML)
- Parses the data into your struct
- Applies defaults
- Validates against your rules
func main() {
// Load from file
var cfg AppConfig
if err := c4.UnmarshalFile("config.yaml", &cfg); err != nil {
log.Fatalf("Config error: %v", err)
}
// Or load from bytes with auto-detection
yamlData := []byte(`
name: "my-app"
port: 3000
debug: true
`)
if err := c4.Unmarshal(yamlData, &cfg); err != nil {
log.Fatalf("Config error: %v", err)
}
// cfg is now valid and ready to use
fmt.Printf("Starting %s on %s:%d\n", cfg.Name, cfg.Host, cfg.Port)
}Error Handling:
If validation fails, Unmarshal returns a ConfigError with detailed field-level messages:
if err := c4.Unmarshal(data, &cfg); err != nil {
if cfgErr, ok := err.(*c4.ConfigError); ok {
for _, e := range cfgErr.Errors {
fmt.Printf("Field %s: %s\n", e.Path, e.Message)
}
}
}Auto-generate markdown documentation directly from your schema definition:
func generateDocs() {
docs := c4.GenerateDocs(&AppConfig{})
// Write to file
if err := os.WriteFile("CONFIG.md", []byte(docs), 0644); err != nil {
log.Fatal(err)
}
}Generated documentation includes:
- Field descriptions
- Type information
- Default values
- Validation rules in human-readable format
- Example YAML config
You can also use the CLI tool to auto-discover all Configurable types:
go run github.com/realharshthakur/c4/cmd/c4-docs@latestAn LSP (Language Server Protocol) server provides IDE features:
- Hover β See field descriptions and validation rules
- Completion β Auto-complete field names and valid enum values
- Diagnostics β Real-time validation errors with squiggles
White-labeling: The LSP server is easily white-labeled and can be shipped as part of your CLI tool. You can also build a branded VS Code extension or vim plugin on top of it β the LSP server handles all the heavy lifting, so the editor extensions are simple wrappers around stdio.
package main
import (
"github.com/realharshthakur/c4/lsp"
)
func main() {
// Define all config types the server should support
configTypes := map[string]any{
"AppConfig": &AppConfig{},
"DatabaseConfig": &DatabaseConfig{},
}
// Configure LSP options
opts := lsp.Options{
Name: "Config Language Server",
Version: "1.0.0",
LanguageID: "yaml", // For VS Code
FilePatterns: []string{
"*.yaml",
"*.yml",
"*.toml",
},
HoverEnabled: true,
CompletionEnabled: true,
DiagnosticsEnabled: true,
}
// Start the server (reads from stdin, writes to stdout)
if err := lsp.Run(configTypes, opts); err != nil {
panic(err)
}
}The LSP server auto-detects which schema to use by looking for a comment at the top of your config file:
# AppConfig
name: my-app
port: 8080
debug: trueThe comment # AppConfig tells the server to validate this file against the AppConfig schema. Make this clear in your documentation.
Create .vscode/settings.json:
{
"[yaml]": {
"editor.defaultFormatter": "redhat.vscode-yaml",
"editor.formatOnSave": true
},
"[toml]": {
"editor.defaultFormatter": "denoland.vscode-deno"
}
}And .vscode/launch.json to debug the server:
{
"version": "0.2.0",
"configurations": [
{
"type": "go",
"name": "Config Language Server",
"request": "launch",
"program": "${workspaceFolder}/cmd/lsp-server",
"cwd": "${workspaceFolder}"
}
]
}Here's a minimal but complete example:
package main
import (
"fmt"
"log"
"os"
"github.com/realharshthakur/c4"
"github.com/realharshthakur/c4/lsp"
)
type ServerConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
}
func (c *ServerConfig) ConfigSchema(b *c4.SchemaBuilder) {
b.Description("Server configuration")
b.Field(&c.Host).Default("localhost").Description("Server hostname")
b.Field(&c.Port).Default(8080).GreaterThan(0).LessThan(65535).Description("Server port")
}
func main() {
if len(os.Args) > 1 && os.Args[1] == "lsp" {
// Start LSP server mode
if err := lsp.Run(map[string]any{"ServerConfig": &ServerConfig{}},
lsp.Options{
Name: "Server Config LSP",
Version: "1.0.0",
HoverEnabled: true,
CompletionEnabled: true,
DiagnosticsEnabled: true,
}); err != nil {
panic(err)
}
return
}
// Load and use config
var cfg ServerConfig
if err := c4.UnmarshalFile("server.yaml", &cfg); err != nil {
log.Fatalf("Config error: %v", err)
}
fmt.Printf("Server: %s:%d\n", cfg.Host, cfg.Port)
// Generate docs
docs := c4.GenerateDocs(&ServerConfig{})
os.WriteFile("SERVER_CONFIG.md", []byte(docs), 0644)
}Usage:
# Generate docs
go run . > SERVER_CONFIG.md
# Start LSP server (editor connects to stdin/stdout)
go run . lsp
# Load and validate config
go run .- Review the
examples/directory for more patterns - Check
AGENTS.mdfor component architecture details - Read the CEL docs for advanced validation: https://github.com/google/cel-spec
We welcome contributions from the community! Whether it's bug reports, feature requests, documentation improvements, or code contributions, please feel free to open an issue or submit a pull request.
Please ensure your contributions:
- Follow the existing code style and patterns
- Include tests for new functionality
- Update documentation as needed
- Reference any related issues in your PR
C4 is licensed under the Apache License 2.0. See LICENSE for details.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.