How Hugo Data File Loading Works: JSON, YAML, TOML, and Templating Guide

Hugo loads JSON, YAML, and TOML files from the data directory at build time, decodes them using the metadecoders package, and exposes the hierarchical structure through the .Data map for O(1) template access.

The gohugoio/hugo static site generator treats the data directory as a hierarchical key-value store, making Hugo data file loading a powerful mechanism for separating structured content from templates. At build start, Hugo walks this directory, unmarshals supported formats, and merges the results into a nested map accessible from any template via dot notation.

The Data Loading Pipeline

The implementation in hugolib/hugo_sites.go orchestrates a three-stage pipeline that transforms files on disk into structured data available to templates.

Discovery and File Walking

The process begins with (*HugoSites).loadData, which creates a hugofs.Walkway to traverse h.PathSpec.BaseFs.Data.Fs. For each file encountered, it invokes (*HugoSites).handleDataFile to process the individual file. This walk happens early in the build process, ensuring all data is available before template execution begins.

Reading and Decoding

Once a file is identified, (*HugoSites).readData handles the I/O and deserialization. The function opens the file, reads its contents using helpers.ReaderToBytes, determines the format via metadecoders.FormatFromString(f.Ext()), and unmarshals the content through metadecoders.Default.Unmarshal. This centralized decoding in parser/metadecoders/decoder.go ensures consistent handling across JSON, YAML, and TOML formats.

Merging into the Data Tree

The final stage occurs in handleDataFile, which constructs the hierarchical data structure. The function extracts path components from r.FileInfo().Meta().PathInfo.Unnormalized().Dir()[1:] to determine the nested location within the data tree. It walks the map, creating sub-maps as needed, and merges the decoded value.

Precedence rules are critical here: when maps contain duplicate keys, Hugo preserves existing keys and emits a warning. However, slices and scalar values are overwritten. This merging strategy ensures that project-level data files in data/ override theme-provided files in the theme's data/ directory, as the project's data directory is processed after the theme's.

Format Handling and Decoding

The parser/metadecoders package provides the abstraction layer for format detection and parsing. The FormatFromString function maps file extensions—.json, .yaml, .yml, and .toml—to internal Format constants. The Default.Unmarshal method then dispatches to the appropriate concrete decoder, ensuring that whether your data resides in JSON, YAML, or TOML, the resulting Go data structures are identical from the template's perspective.

Accessing Data in Templates

Once loaded, the data tree is exposed through two namespaces. The modern approach uses .Data, while .Site.Data remains available but deprecated. The (*Site).Data method in hugolib/site.go forwards to hugo.Data, emitting a deprecation notice when the old path is used.

Because the map is fully constructed before template execution, lookups are O(1) operations using dot notation. For a file at data/blog/authors.json, the template accesses the content via {{ .Data.blog.authors }}. The hierarchical structure mirrors the directory layout, with each subdirectory becoming a nested map key.

Practical Examples

The following examples demonstrate real-world usage patterns for Hugo data file loading.

Example 1: Simple JSON Data File

Create data/authors.json:

{
  "alice": { "name": "Alice", "email": "alice@example.com" },
  "bob":   { "name": "Bob",   "email": "bob@example.com"   }
}

Access in templates:

{{ range $id, $author := .Data.authors }}
  <h2>{{ $author.name }}</h2>
  <p>{{ $author.email }}</p>
{{ end }}

Example 2: Nested Directory with TOML

Create data/team/engineering.toml:

[[member]]
name = "Carol"
role = "Backend"

[[member]]
name = "Dave"
role = "Frontend"

Template usage:

{{ range .Data.team.engineering.member }}
  <p>{{ .name }} – {{ .role }}</p>
{{ end }}

Example 3: Overriding Theme Data with YAML

Theme provides data/settings.yaml:

logo: "/images/theme-logo.png"

Project overrides with data/settings.yaml:

logo: "/images/custom-logo.png"

Template access:

<img src="{{ .Data.settings.logo }}" alt="Site logo">

The project's file takes precedence because Hugo processes the site's data directory after the theme's, overwriting scalar values accordingly.

Example 4: Runtime Resource Loading with transform.Unmarshal

For data files not in the data directory, use resources:

{{ $json := resources.Get "data/metrics.json" | transform.Unmarshal }}
{{ range $metric := $json.metrics }}
  <li>{{ $metric.name }}: {{ $metric.value }}</li>
{{ end }}

The transform.Unmarshal function in tpl/transform/unmarshal.go reuses the same metadecoders logic, providing consistent decoding for resources loaded at template execution time.

Summary

  • Hugo walks the data directory at build start using hugolib/hugo_sites.go, processing every JSON, YAML, and TOML file.
  • The parser/metadecoders package handles format detection and unmarshaling, supporting .json, .yaml, .yml, and .toml extensions.
  • Data merges into a hierarchical map accessible via .Data (preferred) or .Site.Data (deprecated), with project files overriding theme files.
  • Directory structure mirrors the map hierarchy, enabling dot-notation access like {{ .Data.team.engineering }}.
  • For dynamic loading, transform.Unmarshal provides runtime decoding of resource files using the same underlying decoders.

Frequently Asked Questions

What file formats does Hugo support for data files?

Hugo natively supports JSON, YAML, and TOML for data files. The metadecoders.FormatFromString function in parser/metadecoders/decoder.go recognizes file extensions .json, .yaml, .yml, and .toml, automatically selecting the appropriate decoder for each file during the build process.

How do I override a data file provided by a theme?

Place a file with the identical name and path in your project's data directory. Hugo processes the site's data directory after the theme's data directory in (*HugoSites).handleDataFile, causing scalar and slice values to overwrite theme defaults. Map values merge conservatively, preserving existing keys while emitting warnings for collisions.

What is the difference between .Site.Data and .Data?

.Site.Data is the legacy access path, while .Data is the modern namespace introduced to simplify the API. The (*Site).Data method in hugolib/site.go forwards calls to the underlying data structure but emits a deprecation warning when accessed via .Site.Data. Both point to the same hierarchical map built from the data directory.

Can I load data files outside the data directory at build time?

Yes, using the resources.Get function combined with transform.Unmarshal. This approach loads the file as a resource and decodes it using the same metadecoders.Default.Unmarshal logic used for the data directory. The template function defined in tpl/transform/unmarshal.go supports JSON, YAML, and TOML resources located anywhere within the asset directories.

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 →