Here is a possible implementation.
typealias AnyEquatable = AnyHashable // TODO: implement AnyEquatable properly
typealias Dict = [AnyHashable: AnyEquatable]
extension Encodable {
func jsonData(encoder: JSONEncoder? = nil) throws -> Data {
try (encoder ?? JSONEncoder()).encode(self)
}
}
extension Decodable {
static func from(jsonData: Data, decoder: JSONDecoder? = nil) throws -> Self {
try (decoder ?? JSONDecoder()).decode(self, from: jsonData)
}
}
extension JSONEncoder {
var writingOptions: JSONSerialization.WritingOptions {
let opts = outputFormatting
var v: JSONSerialization.WritingOptions = []
v.insert(opts.contains(.prettyPrinted) ? .prettyPrinted : [])
v.insert(opts.contains(.sortedKeys) ? .sortedKeys : [])
v.insert(opts.contains(.withoutEscapingSlashes) ? .withoutEscapingSlashes : [])
return v
}
}
extension Data {
func decodeJsonObject(options: JSONSerialization.ReadingOptions = []) throws -> Any {
try JSONSerialization.jsonObject(with: self, options: options)
}
init(encodeJsonObject object: Any, options: JSONSerialization.WritingOptions = []) throws {
self = try JSONSerialization.data(withJSONObject: object, options: options)
}
}
extension JSONEncoder {
func encode<T: Encodable>(_ value: T, default defValue: T) throws -> Data {
let writingOptions = self.writingOptions
var data = try value.jsonData(encoder: self)
let dict = try data.decodeJsonObject()
let defData = try defValue.jsonData(encoder: self)
let defDict = try defData.decodeJsonObject()
guard var dict = dict as? Dict, let defDict = defDict as? Dict else {
// TODO: use EncodingError
throw NSError(domain: "JSON", code: -1, userInfo: nil)
}
if dict.removeMatchingValues(defDict) {
data = try Data(encodeJsonObject: dict, options: writingOptions)
}
return data
}
}
extension JSONDecoder {
func decode<T: Codable>(_ type: T.Type, default defValue: T, from data: Data) throws -> T {
let dict = try data.decodeJsonObject()
let defData = try defValue.jsonData()
let defDict = try defData.decodeJsonObject()
guard var dict = dict as? Dict, let defDict = defDict as? Dict else {
// TODO: use DecodingError
throw NSError(domain: "JSON", code: -1, userInfo: nil)
}
_ = dict.addMissingValues(defDict)
let newData = try Data(encodeJsonObject: dict)
return try type.from(jsonData: newData, decoder: self)
}
}
extension Dictionary {
mutating func removeMatchingValues(_ dict: Self) -> Bool {
var changed = false
dict.forEach { key, defVal in
if let val = self[key] {
if let val = val as? AnyEquatable, let defVal = defVal as? AnyEquatable, val == defVal {
self[key] = nil
changed = true
} else if var val = val as? Dict, let defVal = defVal as? Dict {
if val.removeMatchingValues(defVal) {
changed = true
self[key] = (val as! Value)
}
}
}
}
return changed
}
mutating func addMissingValues(_ dict: Self) -> Bool {
var changed = false
dict.forEach { key, defVal in
if let val = self[key] {
if var val = val as? Dict, let defVal = defVal as? Dict {
if val.addMissingValues(defVal) {
changed = true
self[key] = (val as! Value)
}
}
} else {
self[key] = defVal
changed = true
}
}
return changed
}
}
This implementation exposes a custom 'JSONEncoder().encode(val, default: defVal)' and ''JSONDecoder().decode(S.self, default: defVal, from: data)" that takes additional "default" value parameter: values missing in JSON are replaced with the corresponding default values and likewise the encoded data doesn't contain default values.
struct S: Codable {
var x: Int = 100
var y: String = "hello"
var a: A = A()
// purposely not using Equatable for this test so it doesn't affect things
func eq(_ other: Self) -> Bool {
x == other.x && y == other.y && a.eq(other.a)
}
}
struct A: Codable {
var a: Int = 300
var b: String = "world"
// purposely not using Equatable for this test so it doesn't affect things
func eq(_ other: Self) -> Bool {
a == other.a && b == other.b
}
}
func testValue(_ val: S, default defVal: S, encoder: JSONEncoder, decoder: JSONDecoder) {
let data = try! encoder.encode(val, default: defVal)
let s = String(data: data, encoding: .utf8)!
print("json: \(s.replacingOccurrences(of: "\\\"", with: "\""))")
let val2 = try! decoder.decode(S.self, default: defVal, from: data)
assert(val2.eq(val))
}
func test() {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let def = S() // default value
testValue(S(x: 1, y: "2", a: A(a: 3, b: "4")), default: def, encoder: encoder, decoder: decoder)
testValue(S(x: 100, y: "hello", a: A(a: 300, b: "world")), default: def, encoder: encoder, decoder: decoder)
testValue(S(x: 1, y: "hello", a: A(a: 3, b: "4")), default: def, encoder: encoder, decoder: decoder)
testValue(S(x: 1, y: "hello", a: A(a: 3, b: "world")), default: def, encoder: encoder, decoder: decoder)
testValue(S(x: 100, y: "hello", a: A(a: 300, b: "good")), default: def, encoder: encoder, decoder: decoder)
print("done")
}
test()
outputs:
json: {"x":1,"y":"2","a":{"a":3,"b":"4"}}
json: {}
json: {"x":1,"a":{"b":"4","a":3}}
json: {"x":1,"a":{"a":3}}
json: {"a":{"b":"good"}}
As you can see this implementation generates compact JSON's without default values and can recreate the original values provided "decode" is called with the same default value parameter as "encode". The code is highly untested, so use on your own risk. Also note the TODO's in code (non critical but worth addressing) IRT "AnyEquatable" and "EncodingError / DecodingError".