Swift(UIKit)中如何优雅过滤API返回JSON的空值与Null值?
Great question! Your current implementation gets the job done, but all that repetitive code for handling characteristic1 through characteristic5 feels unnecessary. Let’s make this more elegant and scalable while keeping all your required fields intact for the UI layer.
1. Extend KeyedDecodingContainer for Reusable Logic
The cleanest fix is to create a custom extension method that handles both null values (via decodeIfPresent) and empty strings in one go. This eliminates the need for your checkEmpty method and reduces repetition in the initializer.
extension KeyedDecodingContainer { /// Decodes a string, returning nil if the value is null OR an empty string func decodeNonEmptyStringIfPresent(forKey key: Key) throws -> String? { guard let stringValue = try decodeIfPresent(String.self, forKey: key) else { return nil } return stringValue.isEmpty ? nil : stringValue } }
Now update your Cat struct’s initializer to use this method (note: I fixed a mapping bug between your JSON's "breedname" and struct's breedName):
struct Cat: Decodable { let breedName: String let picture: String let characteristic1: String? let characteristic2: String? let characteristic3: String? let characteristic4: String? let characteristic5: String? enum CodingKeys: String, CodingKey { case breedName = "breedname" case picture case characteristic1, characteristic2, characteristic3, characteristic4, characteristic5 } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.breedName = try container.decode(String.self, forKey: .breedName) self.picture = try container.decode(String.self, forKey: .picture) // Now each characteristic line is a single, clean call self.characteristic1 = try container.decodeNonEmptyStringIfPresent(forKey: .characteristic1) self.characteristic2 = try container.decodeNonEmptyStringIfPresent(forKey: .characteristic2) self.characteristic3 = try container.decodeNonEmptyStringIfPresent(forKey: .characteristic3) self.characteristic4 = try container.decodeNonEmptyStringIfPresent(forKey: .characteristic4) self.characteristic5 = try container.decodeNonEmptyStringIfPresent(forKey: .characteristic5) } }
2. Use a Custom Wrapper Type (For Cross-Struct Reusability)
If you find yourself needing this non-empty string logic across multiple structs in your codebase, a custom wrapper type is a great way to encapsulate the behavior centrally.
/// A wrapper that converts null values AND empty strings to nil struct NonEmptyString: Decodable { let value: String? init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() do { let stringValue = try container.decode(String.self) self.value = stringValue.isEmpty ? nil : stringValue } catch DecodingError.valueNotFound { // Handle null values gracefully self.value = nil } } }
Then integrate it into your Cat struct:
struct Cat: Decodable { let breedName: String let picture: String let characteristic1: String? let characteristic2: String? let characteristic3: String? let characteristic4: String? let characteristic5: String? enum CodingKeys: String, CodingKey { /* Same as before */ } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.breedName = try container.decode(String.self, forKey: .breedName) self.picture = try container.decode(String.self, forKey: .picture) self.characteristic1 = try container.decode(NonEmptyString.self, forKey: .characteristic1).value self.characteristic2 = try container.decode(NonEmptyString.self, forKey: .characteristic2).value self.characteristic3 = try container.decode(NonEmptyString.self, forKey: .characteristic3).value self.characteristic4 = try container.decode(NonEmptyString.self, forKey: .characteristic4).value self.characteristic5 = try container.decode(NonEmptyString.self, forKey: .characteristic5).value } }
This approach makes it easy to update the logic later (like trimming whitespace before checking for emptiness) without changing every struct that uses it.
3. Bonus: If UI Can Adapt, Use an Array (Optional)
While you mentioned the UI needs to reference fixed individual fields, if you ever get the green light to adjust the UI, you could map the 5 characteristics into an array of non-empty strings. This cuts down on repetition even further:
struct Cat: Decodable { let breedName: String let picture: String let characteristics: [String] // Only contains non-empty, non-null values enum CodingKeys: String, CodingKey { /* Same as before */ } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.breedName = try container.decode(String.self, forKey: .breedName) self.picture = try container.decode(String.self, forKey: .picture) // Collect and filter all characteristics in one go let allCharacteristics = [ try container.decodeNonEmptyStringIfPresent(forKey: .characteristic1), try container.decodeNonEmptyStringIfPresent(forKey: .characteristic2), try container.decodeNonEmptyStringIfPresent(forKey: .characteristic3), try container.decodeNonEmptyStringIfPresent(forKey: .characteristic4), try container.decodeNonEmptyStringIfPresent(forKey: .characteristic5) ] self.characteristics = allCharacteristics.compactMap { $0 } } }
This works best if your UI can display a dynamic list instead of fixed labels for each characteristic.
Final Recommendation
For your current use case (fixed fields required by the UI), the first approach (extending KeyedDecodingContainer) is the best balance of simplicity and cleanliness. It cuts down on repetitive code without introducing extra complexity, and it’s easy to maintain if you add more characteristic fields later.
内容的提问来源于stack exchange,提问作者mendokusai

