Repository navigation
API_DESIGN_GUIDELINES
This document provides guidelines for designing consistent and maintainable JSON configuration structures in Alteriom packages, particularly for StatusPackage and related message types.
- Overview
- Nesting vs Flat Structure Guidelines
- Current Structure Patterns
- Decision Tree
- Examples
- Best Practices
Alteriom packages use JSON serialization for configuration and status data. This document establishes clear patterns for when to use nested structures versus flat key-value pairs to ensure consistency and maintainability across the codebase.
Related Issues:
- Issue #28 - Inconsistent Nested vs Flat Configuration Structure
- Issue #29 - Inconsistent Optional vs Required Field Serialization Pattern
- PR #36 - Documented nesting patterns (this file)
- PR #37 - Removed conditional serialization for predictable JSON structure
- Consistency over perfection - Follow existing patterns in similar sections
- Simplicity by default - Use flat structures unless nesting provides clear benefits
- Future-proof - Consider extensibility when designing structures
- Clarity - Structure should reflect logical grouping
- Predictable structure - All sections always serialize with default values (addressed in PR #37)
- < 4 total fields in a configuration section
- No clear logical subsystems within the section
- Simple value types without complex relationships
- Low likelihood of expansion in the future
Benefits:
- Simpler code (fewer nested object creations)
- Easier to parse and validate
- More concise JSON output
- Faster serialization/deserialization
- 3+ fields belong to same logical subsystem
- Clear semantic grouping exists
- Future extensibility anticipated for subsystem
- Subsystem has distinct meaning separate from parent
Benefits:
- Better logical organization
- Easier to add related fields without cluttering parent
- Clear separation of concerns
- More extensible architecture
Important Note (as of PR #37): All configuration sections now always serialize regardless of whether values are at their defaults. This provides predictable JSON structure and eliminates the need for consumers to check key existence. Default values (0, false, "") clearly indicate "not configured" state.
These sections use simple key-value pairs at a single level:
"display_config": {
"enabled": true,
"brightness": 128,
"timeout_ms": 30000,
"timeout_s": 30
}Rationale: Only 3-4 fields, all directly related to display, no subsystems.
"power_config": {
"deep_sleep_enabled": false,
"deep_sleep_interval_ms": 300000,
"deep_sleep_interval_s": 300,
"battery_percent": 85
}Rationale: Small number of fields (4), even though battery and sleep are different concerns, nesting would add unnecessary complexity.
"mqtt_retry": {
"max_attempts": 5,
"circuit_breaker_ms": 60000,
"circuit_breaker_s": 60,
"hourly_retry_enabled": true,
"initial_retry_ms": 1000,
"initial_retry_s": 1,
"max_retry_ms": 30000,
"max_retry_s": 30,
"backoff_multiplier": 2.0
}Rationale: While this has 9 fields with distinct concerns (retry policy vs backoff strategy), it remains flat for simplicity. The retry configuration is cohesive enough that nesting would fragment it without clear benefit.
These sections use nested objects for logical grouping:
"sensors": {
"read_interval_ms": 30000,
"read_interval_s": 30,
"transmission_interval_ms": 60000,
"transmission_interval_s": 60,
"calibration": {
"temperature_offset": 0.5,
"humidity_offset": -2.0,
"pressure_offset": 0.0
}
}Rationale: Calibration is a distinct subsystem with its own semantic meaning. It's optional, extensible, and conceptually separate from sensor timing configuration.
Benefits of nesting here:
- Calibration can be added/removed as a unit
- Easy to add more calibration fields without cluttering main sensors object
- Clear semantic boundary - calibration is a specific tuning operation
"organization": {
"organizationId": "org-123",
"customerId": "cust-456",
"deviceGroup": "sensors",
"device_name": "sensor-01",
"device_location": "warehouse-a",
"device_secret_set": true
}Rationale: Organization metadata is an optional, self-contained subsystem that may not be present on all devices.
Use this decision tree when designing new configuration sections:
START: New configuration section needed
│
├─ Does section have < 4 fields?
│ ├─ YES → Use FLAT structure
│ └─ NO → Continue
│
├─ Do 3+ fields belong to same logical subsystem?
│ ├─ NO → Use FLAT structure
│ └─ YES → Continue
│
├─ Is subsystem likely to grow in future?
│ ├─ NO → Consider FLAT (unless strong semantic grouping)
│ └─ YES → Continue
│
├─ Would nesting improve clarity significantly?
│ ├─ NO → Use FLAT structure
│ └─ YES → Use NESTED structure
│
END
Scenario: Adding Over-The-Air update configuration with 3 fields.
// C++ Fields
bool otaEnabled = false;
TSTRING otaServer = "";
uint16_t otaPort = 0;
// JSON Serialization (FLAT)
JsonObject ota = jsonObj["ota"].to<JsonObject>();
ota["enabled"] = otaEnabled;
ota["server"] = otaServer;
ota["port"] = otaPort;Result:
"ota": {
"enabled": true,
"server": "ota.example.com",
"port": 8080
}Decision: Keep FLAT - only 3 fields, no subsystems.
Scenario: Adding temperature, humidity, and pressure thresholds to sensor configuration.
// C++ Fields (added to existing sensor config)
double tempThresholdMin = -40.0;
double tempThresholdMax = 85.0;
double humidityThresholdMin = 0.0;
double humidityThresholdMax = 100.0;
// JSON Serialization (NESTED under sensors)
JsonObject sensors = jsonObj["sensors"].to<JsonObject>();
// ... existing sensor fields ...
JsonObject thresholds = sensors["thresholds"].to<JsonObject>();
thresholds["temperature_min"] = tempThresholdMin;
thresholds["temperature_max"] = tempThresholdMax;
thresholds["humidity_min"] = humidityThresholdMin;
thresholds["humidity_max"] = humidityThresholdMax;Result:
"sensors": {
"read_interval_ms": 30000,
"calibration": { ... },
"thresholds": {
"temperature_min": -40.0,
"temperature_max": 85.0,
"humidity_min": 0.0,
"humidity_max": 100.0
}
}Decision: Use NESTED - 4+ fields, clear subsystem (alerting/validation logic), logical grouping.
Scenario: Adding message encoding/compression settings.
// C++ Fields
bool compressionEnabled = false;
TSTRING encodingType = "json";
uint8_t compressionLevel = 6;
// JSON Serialization (FLAT)
JsonObject encoding = jsonObj["encoding"].to<JsonObject>();
encoding["compression_enabled"] = compressionEnabled;
encoding["encoding_type"] = encodingType;
encoding["compression_level"] = compressionLevel;Result:
"encoding": {
"compression_enabled": false,
"encoding_type": "json",
"compression_level": 6
}Decision: Keep FLAT - only 3 fields, cohesive purpose.
Rationale: Flat structures are simpler to implement and consume.
Rule: When in doubt, start flat. Nesting can be added later if needed, but removing nesting is a breaking change.
When adding fields to existing sections:
// Deserialization with backward compatibility
displayTimeout = displayConfig["timeout_ms"] | displayConfig["timeout"] | 0;This allows reading old format (timeout) while preferring new format (timeout_ms).
Good:
"sensors": {
"read_interval_ms": 30000,
"calibration": { ... } // Optional, can be omitted entirely
}Avoid:
"sensors": {
"read_interval_ms": 30000,
"temperature_offset": 0.0, // Mixed levels - unclear if calibration is a concept
"humidity_offset": 0.0,
"pressure_offset": 0.0
}Follow existing conventions:
- Time fields: Follow Time Field Naming Convention
- Boolean fields: Follow Boolean Naming Convention
- Use snake_case for JSON keys
- Use camelCase for C++ field names
Add comments explaining nesting choices:
// Display configuration (flat - only 3 fields, no subsystems)
JsonObject displayConfig = jsonObj["display_config"].to<JsonObject>();
// Sensor configuration with nested calibration (calibration is distinct subsystem)
JsonObject sensors = jsonObj["sensors"].to<JsonObject>();
JsonObject calibration = sensors["calibration"].to<JsonObject>();Create tests to validate structure patterns:
TEST(StatusPackage, StructureConsistency) {
alteriom::StatusPackage pkg;
pkg.tempOffset = 0.5;
JsonDocument doc;
JsonObject obj = doc.to<JsonObject>();
pkg.addTo(std::move(obj));
// Verify nested structures
REQUIRE(obj["sensors"]["calibration"].is<JsonObject>());
// Verify flat structures remain flat
REQUIRE(obj["display_config"]["enabled"].is<bool>());
REQUIRE_FALSE(obj["display_config"].containsKey("nested_section"));
}If restructuring becomes necessary:
- Add new structure while maintaining old structure
- Support both formats in deserialization
- Deprecate old format (document in release notes)
- Remove old format in next major version
// Example: Supporting both flat and nested
if (jsonObj["network"]["wifi"].is<JsonObject>()) {
// New nested format
JsonObject wifi = jsonObj["network"]["wifi"];
wifiSSID = wifi["ssid"].as<TSTRING>();
} else {
// Old flat format (deprecated)
wifiSSID = jsonObj["network"]["wifi_ssid"].as<TSTRING>();
}| Criteria | Flat Structure | Nested Structure |
|---|---|---|
| Field Count | < 4 fields | 3+ fields in subsystem |
| Logical Grouping | No clear subsystems | Clear semantic grouping |
| Extensibility | Low likelihood of growth | Anticipated expansion |
| Complexity | Simple values | Complex relationships |
| Serialization | Always present (PR #37) | Always present (PR #37) |
| Examples | display_config, power_config, ota, encoding, mqtt_retry | sensors.calibration, organization |
Golden Rule: When uncertain, prefer flat structures. Nesting should provide clear organizational or extensibility benefits to justify the added complexity.
Issue #28 identified inconsistent nesting patterns and proposed three options:
- Option 1: Keep Current Structure (Document Pattern) ✅ ADOPTED
- Option 2: Nest Network Configuration (Breaking Change)
- Option 3: Nest MQTT Retry Backoff Settings (Minimal Change)
Decision Rationale:
- Current flat structure for most sections (display_config, power_config, mqtt_retry) is functional and simple
- Only sensors.calibration uses nesting, which is justified by its semantic separation
- Avoiding breaking changes preserves compatibility with existing consumers
- Clear documentation (this file) addresses the inconsistency concern
- PR #37's unconditional serialization provides the predictable structure that was the real underlying concern
Result: The pattern is now documented and validated. Future additions should follow the decision tree in this document.
- 2025-11-04 (PR #37): Updated to reflect unconditional serialization pattern - all sections always serialize with default values for predictable structure
- 2025-11-04 (PR #36): Initial version documenting StatusPackage nesting patterns in response to Issue #28