1

Here's a problem I encounter regularly and don't yet have a good solution for.

I'd like to take arbitrary JSON, with differing structures and key names, and run it through a Swift-based transformation which:

  • Preserves the key names
  • Preserves structure, including null values in the original content
  • Randomly replaces alphanumerics in the keys I specify
  • Handles arrays of strings
  • Handles nested structures

I've had success writing a single-use transformation using Codable, but it requires defining the entire structure ahead of time, so it's not general use for arbitrary JSON. It also needs a custom implementation of encode(to encoder: Encoder) to preserve nulls, which further makes things clunky.

Would the alternative be some approach using string scanning to detect the desired fields and rewrite their contents? Some sort of reflection using dictionaries? Something else?

Danilo Campos
  • 6,430
  • 2
  • 25
  • 19
  • Sorry, there is no simple way, I guess. – vadian May 05 '21 at 14:17
  • Nothing simple. It's not thought as such. I'd say `JSONSerialization`? Or maybe SwiftyJSON? – Larme May 05 '21 at 14:38
  • 1
    Parse into a generic JSON structure and operate on that. For example see https://stackoverflow.com/questions/65901928/swift-jsonencoder-encoding-class-containing-a-nested-raw-json-object-literal/65902852#65902852 (I'm currently doing a lot of work on RNJSON, and it may not be in a fully usable state at any time, but this version is pretty solid: https://github.com/rnapier/RNJSON/blob/0d12f5957f467b458272aed9e20df82affa85682/Sources/RNJSON/RNJSON.swift) – Rob Napier May 05 '21 at 16:46
  • 1
    Can you show an example of simple input and its desired output? – Mojtaba Hosseini May 08 '21 at 13:00
  • what is the source? how it's being generated? is it from an REST API call? is it javascript, Swift, PHP, .... generated? – Jintor May 10 '21 at 21:22
  • 2
    What does "Randomly replaces alphanumerics in the keys I specify" mean? Could mean many things. You need to describe or at least provide some examples. – Steve Waddicor May 11 '21 at 11:15

1 Answers1

0

May be this way will fit for you, with some modifications. First of all we need keep fields which we want to hash. I keep it in secretFields. Also we need function for hash like randomString. And of course we need recursive dynamic decode with HashCodable.

import Foundation

let jsonData = """{"city":"New York","secret_field": {"first name": "Adam","last name":"Smith"}}"""

// [1]
let secretFIelds = ["first name", "last name"]

// [2]
func randomString(length: Int) -> String {
  let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
  return String((0..<length).map{ _ in letters.randomElement()! })
}

// [3]
struct HashCodable: Decodable {
  var value: Any

  struct CodingKeys: CodingKey {
    var stringValue: String
    var intValue: Int?
    init?(intValue: Int) {
      self.stringValue = "\(intValue)"
      self.intValue = intValue
    }
    init?(stringValue: String) { self.stringValue = stringValue }
  }

  init(value: Any) {
    self.value = value
  }

  init(from decoder: Decoder) throws {
    if let container = try? decoder.container(keyedBy: CodingKeys.self) {
      var result = [String: Any]()
      try container.allKeys.forEach { (key) throws in
        if secretFIelds.contains(key.stringValue) {
            result[key.stringValue] = randomString(length:10)
        } else {
            result[key.stringValue] = 
                try container.decode(HashCodable.self, forKey: key).value
        }    
      }
      value = result
    } else if var container = try? decoder.unkeyedContainer() {
      var result = [Any]()
      while !container.isAtEnd {
        result.append(try container.decode(HashCodable.self).value)
      }
      value = result
    } else if let container = try? decoder.singleValueContainer() {
      if let intVal = try? container.decode(Int.self) {
        value = intVal
      } else if let doubleVal = try? container.decode(Double.self) {
        value = doubleVal
      } else if let boolVal = try? container.decode(Bool.self) {
        value = boolVal
      } else if let stringVal = try? container.decode(String.self) {
        value = stringVal
      } else {
        throw DecodingError.dataCorruptedError(
            in: container, debugDescription: 
              "the container contains nothing serialisable")
      }
    } else {
      throw DecodingError.dataCorrupted(
          DecodingError.Context(codingPath: decoder.codingPath, 
            debugDescription: "Could not serialise"))
    }
  }
}

let decoded = try! JSONDecoder().decode(HashCodable.self, from: jsonData.data(using: .utf8)!)
print(decoded)

input JSON:

 {
   "city":"New York",
   "secret_field": {
      "first name": "Adam",
      "last name": "Smith"  
    }
 }```

output:

HashCodable(value: [
  "city": "New York", 
  "secret_fields": [
    "last name": "3R6ocxYS44",
    "first name": "uCFgajZQY7"
   ]
])```
Daniil Loban
  • 4,165
  • 1
  • 14
  • 20