# Zed Settings and Configuration System Architecture: Hierarchical JSON with Compile-Time Type Safety

> Explore Zed's hierarchical JSON settings architecture. Discover compile-time type safety and how configurations merge from multiple sources into a runtime singleton.

- Repository: [Zed Industries/zed](https://github.com/zed-industries/zed)
- Tags: architecture
- Published: 2026-03-01

---

**Zed's settings system uses a hierarchical JSON-based store with compile-time type registration via the `Settings` trait and `RegisterSetting` macro, merging configuration from default assets, global, user, and project files into a runtime singleton accessible through static typed accessors.**

The Zed editor's configuration engine is built around a sophisticated merge system that transforms layered JSON files into type-safe Rust values at runtime. Located in the `zed-industries/zed` repository, this architecture enables granular control from global defaults down to per-project overrides while maintaining compile-time guarantees through procedural macros.

## Core Architecture Components

The settings system is organized into three primary crates that handle storage, data modeling, and registration mechanics.

### The SettingsStore Singleton

At the heart of the system lies `SettingsStore`, a mutable singleton defined in [`crates/settings/src/settings_store.rs`](https://github.com/zed-industries/zed/blob/main/crates/settings/src/settings_store.rs). This structure holds the merged `SettingsContent`, maintains per-file caches, and manages the `setting_values` hash map that stores concrete typed values. According to the zed-industries/zed source code, the store handles the complete lifecycle including loading, saving, migrations, and recomputation when files change.

The public facade in [`crates/settings/src/settings.rs`](https://github.com/zed-industries/zed/blob/main/crates/settings/src/settings.rs) exposes the `init()` function that bootstraps the system:

```rust
pub fn init(cx: &mut App) {
    let settings = SettingsStore::new(cx, &default_settings());
    cx.set_global(settings);
    SettingsStore::observe_active_settings_profile_name(cx).detach();
}

```

This initialization sequence creates a new `SettingsStore` using the embedded [`default.json`](https://github.com/zed-industries/zed/blob/main/default.json) asset, installs it as a global in the application context, and activates profile observation for dynamic switching.

### Data Models and JSON Schema

The underlying data structures that mirror the JSON schema live in [`crates/settings_content/src/lib.rs`](https://github.com/zed-industries/zed/blob/main/crates/settings_content/src/lib.rs). Key types include `SettingsContent`, `UserSettingsContent`, and `ProjectSettingsContent`, which model the full configuration hierarchy. These structures derive `serde::Deserialize` and `serde::Serialize` to enable seamless JSON serialization while preserving Rust's type safety.

For low-level JSON manipulation without full re-parsing, [`crates/settings_json/src/lib.rs`](https://github.com/zed-industries/zed/blob/main/crates/settings_json/src/lib.rs) provides helpers like `edits_for_update` and `update_value_in_json_text`. These functions generate text edits that preserve comments and formatting when updating configuration values programmatically.

## Configuration Hierarchy and Precedence

Zed merges configuration from seven distinct sources in strict precedence order, with later levels overriding earlier ones. The `SettingsStore::recompute_values` method implements this merge logic in [`crates/settings/src/settings_store.rs`](https://github.com/zed-industries/zed/blob/main/crates/settings/src/settings_store.rs).

The precedence stack from lowest to highest is:

1. **Default** – Embedded [`assets/settings/default.json`](https://github.com/zed-industries/zed/blob/main/assets/settings/default.json) bundled with the binary
2. **Extension** – Settings provided by installed extensions
3. **Global** – System-wide configuration at `~/.config/zed/global.json`
4. **User** – Primary user settings at `~/.config/zed/settings.json`
5. **OS and Release Channel** – Platform-specific overlays within the user file
6. **User Profile** – Named profiles selected via `profiles/` directory or configuration
7. **Server** – Remote-side defaults used by the language-server bridge
8. **Project** – Per-project overrides in [`.zed/settings.json`](https://github.com/zed-industries/zed/blob/main/.zed/settings.json) within a worktree

Each source maps to a variant of the `SettingsFile` enum. The store can enumerate all active files via `SettingsStore::get_all_files` and query specific values from individual sources using `get_value_from_file`.

## Type-Safe Settings Registration

The bridge between JSON configuration and Rust code relies on a trait-based registration system that operates at compile time.

### The Settings Trait

Every configurable value implements the `Settings` trait defined in [`crates/settings/src/settings.rs`](https://github.com/zed-industries/zed/blob/main/crates/settings/src/settings.rs). This requires a single method:

```rust
pub trait Settings: 'static + Sized + Send + Sync + Clone {
    fn from_settings(content: &SettingsContent) -> Self;
}

```

For example, a custom feature flag implementation might look like this:

```rust
use settings::{RegisterSetting, Settings};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Deserialize, Serialize, RegisterSetting)]
pub struct MyFeatureEnabled(pub bool);

impl Settings for MyFeatureEnabled {
    fn from_settings(content: &SettingsContent) -> Self {
        Self(content.misc.my_feature.enabled.unwrap_or(false))
    }
}

```

### Compile-Time Registration

The `#[derive(RegisterSetting)]` macro, defined in [`crates/settings_macros/src/lib.rs`](https://github.com/zed-industries/zed/blob/main/crates/settings_macros/src/lib.rs), expands to a static `RegisteredSetting` that stores three callbacks: `settings_value` for creating the container, `from_settings` for transformation, and `id` for type identification. The system uses the `inventory` crate to collect all registrations at compile time.

When `SettingsStore::new` executes, it iterates over all registered types via `inventory::iter!` and populates the `setting_values` map with `SettingValue<T>` containers.

### Accessing Settings at Runtime

Once registered, any setting becomes accessible through a static method on the type itself:

```rust
let font_size = EditorFontSize::get(cx);
println!("Current editor font size: {}pt", font_size.0);

```

The generic `Settings::get` implementation forwards to the global `SettingsStore`, retrieves the typed `SettingValue`, and returns a reference to the concrete value. This pattern allows UI code anywhere in the application to access configuration without passing state through the component tree.

## Runtime Updates and JSON Manipulation

The system supports programmatic updates that preserve file formatting and user comments.

### Hierarchical Merging

When any configuration file changes, `SettingsStore::recompute_values` traverses the precedence hierarchy and produces a single `merged_settings` structure. Each registered type's `from_settings` closure then transforms this merged content into its concrete Rust representation, updating the values stored in `setting_values`.

### Atomic JSON Editing

To modify settings without destroying formatting, use `new_text_for_update` in [`crates/settings/src/settings_store.rs`](https://github.com/zed-industries/zed/blob/main/crates/settings/src/settings_store.rs):

```rust
pub fn new_text_for_update(
    &self,
    old_text: String,
    update: impl FnOnce(&mut SettingsContent),
) -> String {
    let edits = self.edits_for_update(&old_text, update);
    let mut new_text = old_text;
    for (range, replacement) in edits {
        new_text.replace_range(range, &replacement);
    }
    new_text
}

```

For higher-level operations, `update_settings_file` handles the complete workflow:

```rust
use settings::SettingsStore;
use std::sync::Arc;

async fn disable_ai_feature(store: &SettingsStore, fs: Arc<dyn Fs>, cx: &mut App) {
    store.update_settings_file(
        fs,
        |content, _cx| {
            content.project.disable_ai = true.into();
        },
    );
}

```

This builds the edit list, writes the file atomically through the async `setting_file_updates_tx` channel, and triggers a recomputation so `DisableAi::get(cx)` reflects the new value immediately.

## JSON Schema Generation

Zed generates JSON Schema documents at runtime to power autocomplete and validation in the settings UI. The `SettingsStore::json_schema` method constructs a `schemars::SchemaGenerator`, injects dynamic enum values for fonts and themes, and delegates to `UserSettingsContent::json_schema`. Similarly, `SettingsStore::project_json_schema` handles project-specific schemas.

These schemas enable IDE features for both the built-in settings UI and external extensions that query editor capabilities.

## Summary

- **Hierarchical merging** combines seven configuration levels from default assets through per-project [`.zed/settings.json`](https://github.com/zed-industries/zed/blob/main/.zed/settings.json) files, with explicit precedence rules enforced by `SettingsStore::recompute_values`.
- **Type-safe access** is provided via the `Settings` trait and static `get(cx)` methods, ensuring compile-time guarantees for configuration values.
- **Compile-time registration** uses the `RegisterSetting` derive macro and `inventory` crate to automatically wire Rust types into the global store.
- **Atomic updates** preserve JSON formatting and comments through text-level editing via `new_text_for_update` and `update_settings_file`.
- **Runtime schema generation** produces JSON Schema documents for validation and autocomplete using `schemars` integration.

## Frequently Asked Questions

### How does Zed resolve conflicts between user and project settings?

Zed uses a strict precedence hierarchy where project settings in [`.zed/settings.json`](https://github.com/zed-industries/zed/blob/main/.zed/settings.json) always override user settings, which in turn override global and default configurations. The `SettingsStore::recompute_values` method applies this ordering deterministically, ensuring that the most specific context (the current project) takes precedence when values conflict.

### What is the difference between UserSettingsContent and ProjectSettingsContent?

`UserSettingsContent` models the schema for `~/.config/zed/settings.json` and includes fields like themes, font preferences, and global keybindings. `ProjectSettingsContent` represents [`.zed/settings.json`](https://github.com/zed-industries/zed/blob/main/.zed/settings.json) within a worktree and typically contains project-specific overrides such as LSP configurations, formatting rules, and excluded directories. Both structures implement serialization traits and are merged into the unified `SettingsContent` during store initialization.

### How can I access a setting value from my Zed plugin or extension code?

Access any registered setting through its static `get` method, passing the application context: `let value = MySetting::get(cx);`. This requires your setting type to implement the `Settings` trait and derive `RegisterSetting`. The call resolves to the global `SettingsStore` and returns a reference to the current merged value without requiring direct store access.

### Does Zed preserve comments when updating settings.json programmatically?

Yes. The `SettingsStore` uses text-level editing via [`crates/settings_json/src/lib.rs`](https://github.com/zed-industries/zed/blob/main/crates/settings_json/src/lib.rs) rather than full serialization. The `edits_for_update` function generates replacement ranges that modify only the specific JSON values being changed, leaving surrounding whitespace, comments, and formatting intact. This approach ensures that user annotations and custom formatting survive automated updates.