background

Check out an interesting example for Swift Codable apps (see resources below). Here’s what the question looks like:

The field returned by the back end does not match the type agreed upon by the original protocol

  • Expected: Object type
  • Actual: String

This causes the Swift Model to declare this field inconvenient

The reasons for this inconvenience are:

  • 1. If the attribute is declared as String, it needs to be reprocessed from String to the desired model
  • 2. If you declare it as a Model type, decode will fail in Codable due to strong verification requirements for types

Regardless of the rationality of sending JSON strings from the back end, you can try the following two solutions to solve the problem.

Option 1: Custom Decodable implementation

Swift Codable is the protocol layer that Swift encods and decodes between data and model. For convenience, the compiler automatically assembles Encodable and Decodable implementations of the model for model reporting and following Codable for model. There is also a lot of room for customization within specific custom types. Around this case, a preliminary exploration. JSONDecoder will throw an error decoding failure if the data type returned is not the declared expected type. The possible solution here is to complete a custom Decoable implementation for the Model. Supplementary type declarations are as follows:

struct Token: Codable {
    let result: Bool?
    
    // Body object is expected, but string is returned
    let body: Body?
}

struct Body: Codable {
    let serialNumber: String?
    let timestamp: String?
}
Copy the code

The body attribute is the field that needs custom implementation, which is defined in Codable and is the outer type Token. The custom implementation is as follows:

Throw (from decoder: decoder) throws {let container = try decoder. Container (keyedBy: CodingKeys.self) self.result = try container.decodeIfPresent(Bool.self, forKey: .result) let jsonString = try container.decodeIfPresent(String.self, forKey: .body) let jsonStringData = jsonString?.data(using: .utf8) self.body = jsonStringData.flatMap return try? JSONDecoder().decode(Body.self, from: $0) } }Copy the code

The main process here is:

  • Custom decodable implementation
  • Decode this key as a string first
  • Decode the secondary Body object after converting string to data

Scheme 2: further encapsulates the decoding process of JSON String

The problem with the above custom scheme is that it cannot be reused, and if you have multiple fields or multiple models, you need to duplicate the code. In real development, a more elegant solution would be to reprocess the property using a Property wrapper to declare the target type wrapped by the property

struct Token {
    @JSONString
    var body: Body?
}

Copy the code

At its heart lies the property Wrapper feature:

  • Decoding is performed first instead of body type under Codable protocol.
  • A string -> model type conversion is performed inside the Wrapper
  • The compiler via wrappedValue supports seamless retrieval of fields of the target type

A possible implementation of JSONString Property Wrapper is as follows

@propertyWrapper
public struct JSONString<Base: Codable>: Codable {
    public var wrappedValue: Base
    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let string = try? container.decode(String.self),
           let data = string.data(using: .utf8) {
            self.wrappedValue = try LionCoderFactory.newJSONDecoder().decode(Base.self, from: data)
            return
        }
        
        self.wrappedValue = try container.decode(Base.self)
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        let data = try LionCoderFactory.newJSONEncoder().encode(wrappedValue)
        if let string = String(data: data, encoding: .utf8) {
            try container.encode(string)
        }
    }
}
Copy the code

This property Wrapper has additional support compared to the previous implementation of custom decodable:

  • Support extension of generic types to become generic
  • Both string and object can be delivered internally

The code will be maintained on GitHub.

summary

There’s a lot to explore in Codable design with Swift’s language features, as well as open source extensions for Codable, such as BetterCodable, and more. Such extensibility leaves plenty of room for enhanced development in API designs with strong typing requirements.

The resources

Original case: “The background returned a JSON that I hated”

Encoding and Decoding Custom Types

Open source library example: BetterCodable