如何将动态JSON映射至对象?未知属性场景的通用解决方案
Great question! When dealing with dynamic JSON transformations where the output structure can't be predefined (since mappings can completely reshape the input), relying solely on dictionaries can get messy quickly—here's a structured, reusable approach using Swift's protocols, generics, and modular rule-based logic.
核心思路
Instead of tying yourself to fixed Codable structs, we can build a system that separates what needs to be transformed (the input JSON) from how it's transformed (the mapping rules). This abstraction makes the solution flexible enough to handle any input/mapping combination without hardcoding structures.
1. Define a Mapping Rule Protocol
First, create a protocol that represents a single, self-contained mapping operation. Each rule knows exactly how to extract data from the input and produce a value for the output.
protocol JSONMappingRule { /// The key in the final output JSON this rule will populate var outputKey: String { get } /// Takes raw input data and returns the transformed value for the output key func transform(input: [String: Any]) -> Any? }
This protocol lets you create specialized rules for common transformation patterns:
Example Rule Implementations
- Key Renaming + Value Modification:
struct RenameAndTransformRule: JSONMappingRule { let inputKey: String let outputKey: String let valueTransformer: (Any) -> Any? func transform(input: [String: Any]) -> Any? { guard let inputValue = input[inputKey] else { return nil } return valueTransformer(inputValue) } } // Usage: Turn "country_code" into "country" with a capitalized value let countryMappingRule = RenameAndTransformRule( inputKey: "country_code", outputKey: "country", valueTransformer: { ($0 as? String)?.capitalized ?? $0 } )
- Multi-Attribute Combination:
struct CombineAttributesRule: JSONMappingRule { let inputKeys: [String] let outputKey: String let combiner: ([Any]) -> Any? func transform(input: [String: Any]) -> Any? { let values = inputKeys.compactMap { input[$0] } // Only proceed if all required input attributes exist guard values.count == inputKeys.count else { return nil } return combiner(values) } } // Usage: Merge "name" and "age" into a single "nameAge" field let nameAgeMappingRule = CombineAttributesRule( inputKeys: ["name", "age"], outputKey: "nameAge", combiner: { "\($0[0]) (\($0[1]))" } )
2. Build a Reusable Mapper Service
Create a lightweight mapper that takes an input dictionary and a list of rules, then applies each rule to generate the final output.
class JSONMapper { static func map(input: [String: Any], using rules: [JSONMappingRule]) -> [String: Any] { var output = [String: Any]() for rule in rules { if let transformedValue = rule.transform(input: input) { output[rule.outputKey] = transformedValue } } return output } }
Quick Usage Example
// Sample input JSON (converted to a dictionary) let personInput: [String: Any] = [ "name": "Alice", "age": 30, "country_code": "us" ] // Assemble your mapping rules let mappingRules: [JSONMappingRule] = [countryMappingRule, nameAgeMappingRule] // Generate the transformed output let mappedPerson = JSONMapper.map(input: personInput, using: mappingRules) // Result: ["country": "Us", "nameAge": "Alice (30)"]
3. Extend for Nested JSON Structures
If you need to transform nested JSON objects, you can add a rule type that handles recursive mapping:
struct NestedMappingRule: JSONMappingRule { let inputKey: String let outputKey: String let nestedRules: [JSONMappingRule] func transform(input: [String: Any]) -> Any? { guard let nestedInput = input[inputKey] as? [String: Any] else { return nil } return JSONMapper.map(input: nestedInput, using: nestedRules) } }
This lets you handle hierarchical data without hardcoding nested structs—just define rules for the child objects and nest them into the parent rule set.
4. Optional: Add Type-Safe Sugar (For Readability)
While the core system uses [String: Any] for maximum flexibility, you can add extension methods to make rule creation more readable and type-safe for common cases:
extension JSONMappingRule where Self == RenameAndTransformRule { static func rename(_ inputKey: String, to outputKey: String, transformString: @escaping (String) -> String) -> Self { RenameAndTransformRule( inputKey: inputKey, outputKey: outputKey, valueTransformer: { ($0 as? String).map(transformString) ?? $0 } ) } } // Now creating rules is cleaner: let countryRule = JSONMappingRule.rename("country_code", to: "country") { $0.capitalized }
Why This Beats Raw Dictionaries
- Reusability: Rules can be shared across different input types (e.g., a country code transformation works for Person, Company, or Address JSONs)
- Maintainability: Each rule handles one specific task, so debugging or modifying transformations is isolated and straightforward
- Scalability: Adding new transformation types (like default values, filtering, or array mapping) just requires a new
JSONMappingRuleimplementation
内容的提问来源于stack exchange,提问作者avregi

