At the moment, the vast majority of mobile applications are client-server. Everywhere there is loading, synchronization, sending events and the main way of interacting with the server is the exchange of data through the json format.
The Foundation has two mechanisms for data serialization and deserization. Old is NSJsonSerialization
and new is Codable
. The last in the list of advantages has in itself such a wonderful thing as autogenerating keys for json data based on a structure (or class) that implements Codable
( Encodable
, Decodable
) and an initializer for decoding data.
And everything seems to be fine, you can use and rejoice, but the reality is not so simple.
Quite often on the server you can find json like:
{"topLevelObject": { "underlyingObject": 1 }, "Error": { "ErrorCode": 400, "ErrorDescription": "SomeDescription" } }
This is an almost real example from one of the project servers.
For the JsonDecoder
class, JsonDecoder
can specify how to work with snake_case keys, but what to do if we have UpperCamelCase, dash-snake-case, or even a hodgepodge, and do not feel like manually writing the keys?
Fortunately, Apple has provided the ability to configure key conversion before mapping them to the CodingKeys
structure using the JSONDecoder.KeyDecodingStrategy
. We will take advantage of this.
To begin with, we will create a structure that implements the CodingKey
protocol, because there is no such structure in the standard library:
struct AnyCodingKey: CodingKey { var stringValue: String var intValue: Int? init(_ base: CodingKey) { self.init(stringValue: base.stringValue, intValue: base.intValue) } init(stringValue: String) { self.stringValue = stringValue } init(intValue: Int) { self.stringValue = "\(intValue)" self.intValue = intValue } init(stringValue: String, intValue: Int?) { self.stringValue = stringValue self.intValue = intValue } }
Then you need to separately handle each case of our keys. Major:
snake_case, dash-snake-case, lowerCamelCase and UpperCamelCase. Check, run, everything works.
Then we encounter a rather expected problem: abbreviations in camelCases (remember numerous id
, Id
, ID
). To make it work, you need to convert them correctly and introduce a rule - abbreviations will be converted to camelCase, keeping only the first letter large and myABBRKey will turn into myAbbrKey .
This solution works great for combinations of several cases.
Note: Implementation will be provided in. .custom
key decoding strategy.
static func convertToProperLowerCamelCase(keys: [CodingKey]) -> CodingKey { guard let last = keys.last else { assertionFailure() return AnyCodingKey(stringValue: "") } if let fromUpper = convertFromUpperCamelCase(initial: last.stringValue) { return AnyCodingKey(stringValue: fromUpper) } else if let fromSnake = convertFromSnakeCase(initial: last.stringValue) { return AnyCodingKey(stringValue: fromSnake) } else { return AnyCodingKey(last) } }
The next routine problem is the method of passing dates. There are a lot of microservices on the server, a few less commands, but also a decent amount and as a result we find ourselves in front of a bunch of date formats like “yes, I use the standard”. In addition, someone sends the dates in a string, someone in Epoch-time. As a result, we again have a hodgepodge of combinations of string-numbers-timezone-millisecond delimiters, and DateDecoder
in iOS complains and requires a strict date format. The solution here is simple, just look through the signs of one format or another and combine them, eventually obtaining the necessary. These formats successfully and completely covered my cases.
Note: This is a custom DateFormatter initializer. Formatted to create formatter
static let onlyDate = DateFormatter(format: "yyyy-MM-dd") static let full = DateFormatter(format: "yyyy-MM-dd'T'HH:mm:ss.SSSx") static let noWMS = DateFormatter(format: "yyyy-MM-dd'T'HH:mm:ssZ") static let noWTZ = DateFormatter(format: "yyyy-MM-dd'T'HH:mm:ss.SSS") static let noWMSnoWTZ = DateFormatter(format: "yyyy-MM-dd'T'HH:mm:ss")
We attach it to our decoder with the help of JSONDecoder.DateDecodingStrategy
and we get a decoder that processes almost anything and converts it into a digestible format for us.
Tests were performed for json strings of 7944 bytes in size.
convertFromSnakeCase strategy | anyCodingKey strategy | |
---|---|---|
Absolute | 0.00170 | 0.00210 |
Relative | 81% | 100% |
As we can see, the custom Decoder
slower applied by 20% due to the mandatory verification of each key in json for the need to transform. However, this is a small price to pay for the absence of the need to explicitly write keys for data structures, implementing Codable
. The number of boilerplate has been greatly reduced in the project with the addition of this decoder. Should I use it to save developer time, but worsen production? You decide.
Full sample code in the library on github
Source: https://habr.com/ru/post/449870/
All Articles