Skip to main content

Configuration Merge Rules

This document specifies how configuration from multiple sources is merged and resolved.

Merge Precedence

Configuration is resolved from multiple sources in this order (highest precedence first):

  1. Command-line flags: --config key=value
  2. Environment variables: MORPHIR__SECTION__KEY
  3. Workspace local config: ./.config/morphir/config.toml
  4. Project config: ./morphir.toml in project directory
  5. Workspace config: ./morphir.toml at workspace root
  6. Parent configs: Walk up directory tree
  7. User config: ~/.config/morphir/config.toml
  8. System config: /etc/morphir/config.toml

Higher precedence sources override lower precedence sources.

Merge Strategies

Different configuration types use different merge strategies:

TypeStrategyDescription
ScalarReplaceHigher precedence value replaces lower
ArrayReplace (default)Higher precedence array replaces entirely
ArrayAppendArrays are concatenated
TableDeep mergeTables are recursively merged

Scalar Values (Replace)

Scalar values (strings, numbers, booleans) are replaced entirely:

# User config (~/.config/morphir/config.toml)
[codegen]
output_format = "compact"

# Project config (./morphir.toml)
[codegen]
output_format = "pretty"

# Result: output_format = "pretty"

Arrays (Replace by Default)

Arrays are replaced entirely by default:

# Workspace config
[codegen]
targets = ["typescript", "scala"]

# Project config
[codegen]
targets = ["spark"]

# Result: targets = ["spark"]

Arrays (Append Mode)

To append instead of replace, use the + prefix:

# Workspace config
[codegen]
targets = ["typescript"]

# Project config
[codegen]
"+targets" = ["spark", "scala"]

# Result: targets = ["typescript", "spark", "scala"]

Tables (Deep Merge)

Tables are recursively merged:

# Workspace config
[codegen.typescript]
module_format = "esm"
strict = true

# Project config
[codegen.typescript]
declaration = true

# Result:
# [codegen.typescript]
# module_format = "esm"
# strict = true
# declaration = true

Section-Specific Rules

[project] Section

The [project] section is never inherited. Each project must define its own:

  • name
  • version
  • source_directory
  • exposed_modules

These fields are project-specific and cannot be overridden from workspace or parent configs.

[workspace] Section

The [workspace] section exists only at workspace root. It is not inherited by member projects.

[dependencies] Section

Dependencies merge with these rules:

  1. Same package, different versions: Project version wins
  2. Path vs version: More specific (project) wins
  3. New dependencies: Added to merged set
# Workspace config
[dependencies]
"morphir/sdk" = "^3.0.0"
"org/shared" = "^1.0.0"

# Project config
[dependencies]
"morphir/sdk" = "^3.1.0" # Overrides workspace
"org/project-specific" = "^2.0.0" # Added

# Result:
# "morphir/sdk" = "^3.1.0"
# "org/shared" = "^1.0.0"
# "org/project-specific" = "^2.0.0"

[codegen] Section

Code generation settings merge deeply:

# Workspace config
[codegen]
targets = ["typescript"]
output_format = "pretty"

[codegen.typescript]
module_format = "esm"

# Project config
[codegen]
"+targets" = ["spark"] # Append

[codegen.typescript]
strict = false # Add to typescript config

[codegen.spark]
spark_version = "3.5" # New target config

# Result:
# targets = ["typescript", "spark"]
# output_format = "pretty"
# [codegen.typescript]
# module_format = "esm"
# strict = false
# [codegen.spark]
# spark_version = "3.5"

[extensions] Section

Extensions merge with these rules:

  1. Same extension ID: Project config wins entirely
  2. Disabled extensions: enabled = false prevents loading
  3. New extensions: Added to merged set
# Workspace config
[extensions]
spark-codegen = { path = "./extensions/spark-codegen.wasm" }

[extensions.spark-codegen.config]
spark_version = "3.4"

# Project config
[extensions.spark-codegen.config]
spark_version = "3.5" # Overrides

# Result: spark_version = "3.5"

[tasks] Section

Tasks merge with these rules:

  1. Same task name: Project definition wins
  2. Hooks: All hooks at all levels run (workspace first, then project)
  3. Dependencies: Resolved from merged task set
# Workspace config
[tasks.lint]
run = "elm-review"

[tasks."pre:build"]
run = "echo 'Workspace pre-build'"

# Project config
[tasks.lint]
run = "elm-review --fix" # Overrides workspace

[tasks."pre:build"]
run = "echo 'Project pre-build'" # Both run!

# Result:
# - lint runs: "elm-review --fix"
# - pre:build runs BOTH (workspace first, then project)

[frontend] Section

Frontend settings merge deeply, with rules evaluated in order:

# Workspace config
[frontend]
language = "elm"

[[frontend.rules]]
pattern = "**/*.morphir"
language = "morphir-dsl"

# Project config
[frontend]
# language inherited from workspace

[[frontend.rules]]
pattern = "src/legacy/**"
language = "elm" # Project rules evaluated first

# Result: Project rules checked first, then workspace rules

Environment Variables

Environment variables override file configuration using this naming convention:

MORPHIR__<SECTION>__<KEY>

Naming Rules

  • Sections and keys are uppercase
  • Dots become double underscores
  • Hyphens become single underscores

Examples

Environment VariableConfiguration Path
MORPHIR__PROJECT__NAMEproject.name
MORPHIR__CODEGEN__OUTPUT_FORMATcodegen.output_format
MORPHIR__CODEGEN__TYPESCRIPT__STRICTcodegen.typescript.strict
MORPHIR__FRONTEND__LANGUAGEfrontend.language

Type Coercion

Environment variables are strings. They are coerced to the expected type:

Expected TypeCoercion
stringAs-is
integerParse as integer
boolean"true", "1", "yes" → true; else false
arrayJSON array or comma-separated
# String
export MORPHIR__PROJECT__NAME="my-org/my-project"

# Integer
export MORPHIR__WORKSPACE__MAX_JOBS="4"

# Boolean
export MORPHIR__IR__STRICT_MODE="true"

# Array (JSON)
export MORPHIR__CODEGEN__TARGETS='["typescript","scala"]'

# Array (comma-separated)
export MORPHIR__CODEGEN__TARGETS="typescript,scala"

Command-Line Overrides

Command-line flags have highest precedence:

# Override single value
morphir build --config codegen.output_format=compact

# Override nested value
morphir build --config codegen.typescript.strict=false

# Override array (JSON)
morphir build --config 'codegen.targets=["spark"]'

# Multiple overrides
morphir build \
--config codegen.output_format=compact \
--config ir.strict_mode=true

Merge Algorithm

function mergeConfig(sources: ConfigSource[]): Config {
result = {}

// Process sources from lowest to highest precedence
for source in sources.reverse() {
for section in source.sections {
if section.name == "project" || section.name == "workspace" {
// Never inherit project/workspace sections
if source.isProjectConfig {
result[section.name] = section.value
}
} else {
result[section.name] = mergeSection(
result[section.name],
section.value,
section.name
)
}
}
}

return result
}

function mergeSection(base: Value, overlay: Value, path: string): Value {
if overlay is null {
return base
}

if base is null {
return overlay
}

if overlay is Scalar {
return overlay // Replace
}

if overlay is Array {
if path.startsWith("+") {
return concat(base, overlay) // Append
}
return overlay // Replace
}

if overlay is Table {
result = copy(base)
for key, value in overlay {
result[key] = mergeSection(result[key], value, key)
}
return result
}
}

Examples

Example 1: Workspace with Project Override

Workspace config (workspace/morphir.toml):

[morphir]
version = "^4.0.0"

[workspace]
members = ["packages/*"]

[codegen]
targets = ["typescript"]
output_format = "pretty"

[codegen.typescript]
module_format = "esm"

Project config (workspace/packages/api/morphir.toml):

[project]
name = "my-org/api"
version = "1.0.0"

[codegen]
"+targets" = ["openapi"] # Append

[codegen.typescript]
strict = true # Add to typescript config

Resolved config for packages/api:

[morphir]
version = "^4.0.0"

[project]
name = "my-org/api"
version = "1.0.0"

[codegen]
targets = ["typescript", "openapi"]
output_format = "pretty"

[codegen.typescript]
module_format = "esm"
strict = true

Example 2: Environment Override

Project config:

[codegen]
targets = ["typescript"]

[codegen.typescript]
strict = true

Environment:

export MORPHIR__CODEGEN__TARGETS="spark,scala"
export MORPHIR__CODEGEN__TYPESCRIPT__STRICT="false"

Resolved config:

[codegen]
targets = ["spark", "scala"] # From env

[codegen.typescript]
strict = false # From env

Example 3: User Defaults

System config (/etc/morphir/config.toml):

[codegen]
output_format = "compact"

User config (~/.config/morphir/config.toml):

[codegen]
output_format = "pretty"

[ir]
include_source_locations = true

Project config (./morphir.toml):

[project]
name = "my-org/project"
version = "1.0.0"

[codegen]
targets = ["typescript"]

Resolved config:

[project]
name = "my-org/project"
version = "1.0.0"

[codegen]
targets = ["typescript"]
output_format = "pretty" # From user config

[ir]
include_source_locations = true # From user config

Debugging Configuration

Show Resolved Config

# Show fully resolved configuration
morphir config show

# Show specific section
morphir config show codegen

# Show where values come from
morphir config show --sources

Example Output

$ morphir config show --sources

[project]
name = "my-org/api" # ./morphir.toml
version = "1.0.0" # ./morphir.toml

[codegen]
targets = ["typescript", "openapi"] # merged
- "typescript" # ../morphir.toml (workspace)
- "openapi" # ./morphir.toml (append)
output_format = "pretty" # ../morphir.toml (workspace)

[codegen.typescript]
module_format = "esm" # ../morphir.toml (workspace)
strict = true # ./morphir.toml