import json
from typing import Any, Dict, Optional, Union
from pathlib import Path
class ConfigManager:
"""A robust configuration manager that loads, validates, and provides default values for JSON config files."""
def __init__(self, config_path: Union[str, Path], schema: Dict[str, Any]):
"""
Initialize the configuration manager.
Args:
config_path: Path to the JSON configuration file
schema: Dictionary defining expected keys, types, and default values
"""
self.config_path = Path(config_path)
self.schema = schema
self._config = {}
self.load_config()
def load_config(self) -> None:
"""Load configuration from file and validate against schema."""
# Load config file if it exists, otherwise start with empty dict
if self.config_path.exists():
with open(self.config_path, 'r') as f:
try:
file_config = json.load(f)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in config file: {e}")
else:
file_config = {}
# Validate and apply defaults
self._config = self._validate_and_apply_defaults(file_config)
# Save back to file with defaults filled in
self.save_config()
def _validate_and_apply_defaults(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""Validate config against schema and apply default values where needed."""
validated_config = {}
for key, spec in self.schema.items():
# Extract type, default, and required status from schema
expected_type = spec.get('type')
default_value = spec.get('default')
required = spec.get('required', False)
# Get value from config or use default
if key in config:
value = config[key]
# Type checking
if expected_type and not isinstance(value, expected_type):
raise TypeError(f"Config key '{key}' should be of type {expected_type.__name__}, got {type(value).__name__}")
validated_config[key] = value
elif default_value is not None:
validated_config[key] = default_value
elif required:
raise ValueError(f"Required config key '{key}' is missing and has no default")
else:
# Key not present, not required, and no default - skip it
pass
return validated_config
def get(self, key: str, default: Any = None) -> Any:
"""Get a configuration value with an optional default."""
return self._config.get(key, default)
def set(self, key: str, value: Any) -> None:
"""Set a configuration value."""
# Validate type if key exists in schema
if key in self.schema:
expected_type = self.schema[key].get('type')
if expected_type and not isinstance(value, expected_type):
raise TypeError(f"Value for '{key}' should be of type {expected_type.__name__}")
self._config[key] = value
def save_config(self) -> None:
"""Save current configuration to file."""
# Create parent directories if they don't exist
self.config_path.parent.mkdir(parents=True, exist_ok=True)
with open(self.config_path, 'w') as f:
json.dump(self._config, f, indent=2, sort_keys=True)
@property
def config(self) -> Dict[str, Any]:
"""Get a copy of the entire configuration."""
return self._config.copy()
# Example usage
if __name__ == "__main__":
# Define configuration schema
config_schema = {
"debug": {
"type": bool,
"default": False,
"required": False
},
"port": {
"type": int,
"default": 8000,
"required": False
},
"database_url": {
"type": str,
"default": "sqlite:///app.db",
"required": True
},
"max_connections": {
"type": int,
"default": 100,
"required": False
},
"features": {
"type": list,
"default": [],
"required": False
}
}
# Initialize config manager
config = ConfigManager("app_config.json", config_schema)
# Access configuration values
print(f"Debug mode: {config.get('debug')}")
print(f"Server port: {config.get('port')}")
print(f"Database URL: {config.get('database_url')}")
# Modify configuration
config.set('debug', True)
config.set('port', 3000)
# Save changes
config.save_config()
print("Configuration saved!")
This snippet implements a robust configuration manager that handles loading, validating, and managing application settings stored in a JSON file. It provides a clean interface for accessing configuration values while ensuring type safety and applying default values where appropriate.
The ConfigManager class:
Configuration management is a common need in applications, but doing it properly requires handling many details:
This implementation addresses all these concerns in a reusable way. It’s especially valuable for:
config_manager.py)python config_manager.pyapp_config.json file with default valuesTo use in your own project:
ConfigManager instance with your schemaconfig.get() to retrieve values and config.set() to update themconfig.save_config() to persist changesThe first run will create a configuration file with defaults. Subsequent runs will load existing values while maintaining type safety and applying any new defaults from the schema.