How KCL Generates JSON and YAML Output from .k Configuration Files

KCL generates JSON and YAML by evaluating .k files into runtime ValueRef objects, then serializing them through dedicated formatter pipelines in the runtime crate that use serde_json and serde_yaml.

When you execute a KCL program from the kcl-lang/kcl repository, the compiler transforms your configuration through several stages before emitting standard JSON or YAML documents. This pipeline involves parsing source code into an abstract syntax tree, evaluating it into runtime values, and finally invoking Rust-based serializers that respect formatting options like indentation and private field exclusion.

The KCL Output Pipeline: From Parsing to Serialization

KCL follows a strict multi-stage architecture to transform human-readable configuration into machine-readable output.

Parsing and Evaluation

The process begins in the compiler front-end. The lexer (crates/lexer) tokenizes the source text, and the parser (crates/parser) constructs an AST. The resolver (crates/sema) then annotates symbols with type information before the evaluator (crates/evaluator) walks the tree, creating runtime values represented as ValueRef objects.

These ValueRef instances hold the actual configuration data—scalars, lists, and dictionaries—that will eventually be serialized.

Triggering JSON or YAML Generation

The runtime supports two distinct mechanisms to initiate serialization:

  • Format specifiers in string interpolation (#json, #yaml)
  • Standard library functions (json.encode, yaml.encode)

In crates/evaluator/src/node.rs, the evaluator matches format specifiers during string interpolation:

match format_spec {
    "#json" => formatted_expr_value.to_json_string(),
    "#yaml" => formatted_expr_value.to_yaml_string(),
    _ => …
}

Both paths ultimately invoke the same underlying formatter functions exposed via C-FFI.

JSON Encoding Implementation

The JSON pipeline lives entirely within crates/runtime/src/json/mod.rs and crates/runtime/src/value/val_json.rs.

FFI Entry Points

The function kcl_json_encode serves as the external boundary. Defined in crates/runtime/src/json/mod.rs, this unsafe C-FFI wrapper accepts a context pointer, arguments, and keyword arguments:

pub unsafe extern "C-unwind" fn kcl_json_encode(
    ctx, args, kwargs,
) -> *const kcl_value_ref_t {
    // …
    let s = ValueRef::str(arg0.to_json_string_with_options(&opts).as_ref());
    s.into_raw(ctx)
}

The function parses options via args_to_opts to build a JsonEncodeOptions struct, then delegates to the value's stringification method.

Value-to-JSON Conversion

The heavy lifting occurs in crates/runtime/src/value/val_json.rs within to_json_string_with_options. This method traverses the ValueRef tree, converting each node into a serde_json::Value, then delegates to serde_json::to_string for the final encoding.

Configuration Options

The JsonEncodeOptions struct controls output formatting:

  • sort_keys – Alphabetically sort object keys
  • ignore_private – Omit fields prefixed with kcl_ (private variables)
  • ignore_none – Drop null or None values from the output
  • indent – Pretty-print with specified indentation spaces

YAML Encoding Implementation

The YAML pipeline mirrors the JSON implementation but uses serde_yaml and supports multi-document streams.

FFI Entry Points

In crates/runtime/src/yaml/mod.rs, the kcl_yaml_encode function provides the C-FFI boundary:

pub unsafe extern "C-unwind" fn kcl_yaml_encode(
    ctx, args, kwargs,
) -> *const kcl_value_ref_t {
    // …
    let s = ValueRef::str(arg0.to_yaml_string_with_options(&opts).as_ref());
    s.into_raw(ctx)
}

Like its JSON counterpart, it uses args_to_opts to construct YamlEncodeOptions.

Value-to-YAML Conversion

The conversion logic resides in crates/runtime/src/value/val_yaml.rs. The to_yaml_string_with_options method walks the ValueRef tree, mapping it to serde_yaml::Value types before final serialization.

Multi-Document Streams

When encoding multiple values, KCL joins documents using the constant YAML_STREAM_SEP = "\n---\n", producing valid multi-document YAML streams compliant with the YAML specification.

Practical Usage: CLI and Code Examples

You can trigger JSON and YAML generation through the command line or directly within your .k files.

Command-Line Output Flags

The kcl run command provides shorthand flags for direct serialization:


# Output JSON to STDOUT

kcl run config.k -J json

# Output YAML to STDOUT  

kcl run config.k -Y yaml

Inline Format Specifiers

Use interpolation specifiers to convert values inside your configuration:

result = {"name": "demo", "count": 3}
json_str = "${result: #json}"
yaml_str = "${result: #yaml}"

This produces {"name":"demo","count":3} and name: demo\ncount: 3 respectively.

Standard Library Encoding

For programmatic control over formatting options, import the encoding modules:

import "json"
import "yaml"

data = {"a": 1, "b": [2, 3], "kcl_internal": "secret"}

# Pretty-printed JSON with 2-space indentation

json_out = json.encode(data, indent=2)

# YAML excluding private fields (removes kcl_internal)

yaml_out = yaml.encode(data, ignore_private=true)

Both json.encode and yaml.encode accept options for sort_keys, ignore_none, and ignore_private, giving you fine-grained control over the serialized output.

Summary

  • KCL parses .k files into an AST, then evaluates them into ValueRef runtime values stored in the evaluator crate.
  • JSON generation flows through kcl_json_encode in crates/runtime/src/json/mod.rs and converts values via val_json.rs using serde_json.
  • YAML generation follows an identical pattern in crates/runtime/src/yaml/mod.rs and val_yaml.rs, utilizing serde_yaml and supporting multi-document streams with "\n---\n" separators.
  • Both formats respect encoding options including ignore_private (hiding kcl_ prefixed fields), ignore_none, sort_keys, and indentation controls.
  • You can trigger output via CLI flags (-J, -Y), format specifiers (#json, #yaml), or stdlib functions (json.encode, yaml.encode).

Frequently Asked Questions

How does KCL convert internal values to JSON format?

KCL converts internal ValueRef objects to JSON by calling to_json_string_with_options in crates/runtime/src/value/val_json.rs. This function traverses the value tree, maps each node to a serde_json::Value, and uses serde_json::to_string to produce the final text, respecting options like indentation and key sorting passed through the C-FFI boundary from kcl_json_encode.

Can I control indentation when generating YAML output?

Yes. When calling yaml.encode() or using the #yaml format specifier, pass the indent option to control spacing. The YAML encoder in crates/runtime/src/value/val_yaml.rs passes this configuration to serde_yaml, which handles the actual whitespace formatting in the generated document.

What is the difference between using #json and json.encode()?

Format specifiers like #json are evaluated during string interpolation in the evaluator (crates/evaluator/src/node.rs) and are useful for embedding JSON inside larger strings. The json.encode() function is a standard library call that returns a raw string value and accepts options like indent and ignore_private, making it better suited for configuring output files or API responses with specific formatting requirements.

How does KCL handle multiple YAML documents in a single output?

When encoding a list of values or multiple top-level objects, the YAML formatter in crates/runtime/src/yaml/mod.rs joins individual documents using the YAML_STREAM_SEP constant ("\n---\n"). This produces a valid multi-document YAML stream where each document is separated by a line containing three hyphens, allowing you to generate complex Kubernetes manifest bundles or configuration sequences from a single KCL program.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →