Generic JSONDecoder

    At the moment, the vast majority of mobile applications are client-server. Everywhere there is loading, synchronization, sending events and the main way to interact with the server is to exchange data using the json format.


    Key decoding


    The Foundation has two mechanisms for serializing-de-dead data. Old - NSJsonSerializationand new - Codable. The last in the list of advantages has such a wonderful thing in it as auto-generation of 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 enjoy, but the reality is not so simple.
    Quite often on the server you can see json of the form:


    {"topLevelObject":
      {
        "underlyingObject": 1
      },
      "Error": {
        "ErrorCode": 400,
        "ErrorDescription": "SomeDescription"
      }
    }

    This is an almost real-life example from one of the project servers.


    For the class, JsonDecoderyou can specify the work with snake_case keys, but what if we have UpperCamelCase, dash-snake-case, or even a hodgepodge, but we don’t want to write the keys manually?


    Fortunately, Apple provided the ability to configure key mapping before mapping it to a CodingKeysstructure using JSONDecoder.KeyDecodingStrategy. We will take advantage of this.


    First, create a structure that implements the protocol CodingKey, because there is none 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 it is necessary to separately process each case of our keys. Basic:
    snake_case, dash-snake-case, lowerCamelCase and UpperCamelCase. Check, run, everything works.


    Then, faced with the problem quite expected: the abbreviation in camelCase'ah (remember the numerous id, Id, ID). To make it work, you need to correctly convert them and introduce a rule - abbreviations are converted to camelCase, keeping only the first letter capital and myABBRKey will turn into myAbbrKey .


    This solution works great for combinations of several cases.


    Note: Implementation will be provided into .customkey 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)
      }
    }

    Date decoding


    The next routine problem is the way dates are passed. There are a lot of microservices on the server, there are slightly fewer teams, but also a decent amount, and in the end we are faced with a bunch of date formats like “Yes, I use the standard”. In addition, someone passes dates in a string, someone in Epoch-time. As a result, we again have a hodgepodge of string-number-timezone-millisecond-separator combinations, and DateDecoderin iOS it complains and requires a strict date format. The solution here is simple, just by searching we look for signs of a particular format and combine them, getting the necessary result. These formats have successfully and completely covered my cases.


    Note: This is custom DateFormatter initializer. Its just set format to created 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 fasten this to our decoder with the help JSONDecoder.DateDecodingStrategyand we get a decoder that processes almost anything and converts it into a format that is digestible for us.


    Performance tests


    Tests were performed for json strings of size 7944 bytes.


    convertFromSnakeCase strategyanyCodingKey strategy
    Absolute0.001700.00210
    Relative81%100%

    As we can see, custom is Decoder20% slower due to the mandatory verification of each key in json for the need for transformation. However, this is a small fee for not having to explicitly prescribe keys for data structures when implementing Codable. The number of boiler plate is very much reduced in the project with the addition of this decoder. Should I use it to save developer time, but worsen performance? You decide.


    Full sample code in github library


    English article


    Also popular now: