3

I have a class in Swift whose structure resembles this:

class MyClass {
  var name: String
  var data: String
}

Which could be initialised where data contains a JSON object encoded as a String.

var instance = MyClass()
instance.name = "foo"
instance.data = "{\"bar\": \"baz\"}"

I'd now like to serialise this instance using JSONEncoder, I'd get an output similar to this:

{
  "name": "foo",
  "data": "{\"bar\": \"baz\"}"
}

However, what I'd really like

{
  "name": "foo",
  "data": {
    "bar": "baz"
  }
}

Can I achieve this with JSONEncoder? (without changing the data type away from String)

3
  • Do you know the structure of data? Commented Jan 26, 2021 at 13:14
  • No, which is why I don't what to make an effort of decoding/encoding it. Commented Jan 26, 2021 at 13:20
  • (Almost) everything is possible, but the less standard the model is the more code you have to write. As you are going to encode the stuff use a more suitable design. Commented Jan 26, 2021 at 13:28

2 Answers 2

3

You'll first need to decode data as generic JSON. That's a bit tedious, but not too difficult. See RNJSON for a version I wrote, or here's a stripped-down version that handles your issues.

enum JSON: Codable {
    struct Key: CodingKey, Hashable {
        let stringValue: String
        init(_ string: String) { self.stringValue = string }
        init?(stringValue: String) { self.init(stringValue) }
        var intValue: Int? { return nil }
        init?(intValue: Int) { return nil }
    }

    case string(String)
    case number(Double) // FIXME: Split Int and Double
    case object([Key: JSON])
    case array([JSON])
    case bool(Bool)
    case null

    init(from decoder: Decoder) throws {
        if let string = try? decoder.singleValueContainer().decode(String.self) { self = .string(string) }
        else if let number = try? decoder.singleValueContainer().decode(Double.self) { self = .number(number) }
        else if let object = try? decoder.container(keyedBy: Key.self) {
            var result: [Key: JSON] = [:]
            for key in object.allKeys {
                result[key] = (try? object.decode(JSON.self, forKey: key)) ?? .null
            }
            self = .object(result)
        }
        else if var array = try? decoder.unkeyedContainer() {
            var result: [JSON] = []
            for _ in 0..<(array.count ?? 0) {
                result.append(try array.decode(JSON.self))
            }
            self = .array(result)
        }
        else if let bool = try? decoder.singleValueContainer().decode(Bool.self) { self = .bool(bool) }
        else if let isNull = try? decoder.singleValueContainer().decodeNil(), isNull { self = .null }
        else { throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [],
                                                                       debugDescription: "Unknown JSON type")) }
    }

    func encode(to encoder: Encoder) throws {
        switch self {
        case .string(let string):
            var container = encoder.singleValueContainer()
            try container.encode(string)
        case .number(let number):
            var container = encoder.singleValueContainer()
            try container.encode(number)
        case .bool(let bool):
            var container = encoder.singleValueContainer()
            try container.encode(bool)
        case .object(let object):
            var container = encoder.container(keyedBy: Key.self)
            for (key, value) in object {
                try container.encode(value, forKey: key)
            }
        case .array(let array):
            var container = encoder.unkeyedContainer()
            for value in array {
                try container.encode(value)
            }
        case .null:
            var container = encoder.singleValueContainer()
            try container.encodeNil()
        }
    }
}

With that, you can decode the JSON and then re-encode it:

extension MyClass: Encodable {
    enum CodingKeys: CodingKey {
        case name, data
    }
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)

        let json = try JSONDecoder().decode(JSON.self, from: Data(data.utf8))
        try container.encode(json, forKey: .data)
    }
}
Sign up to request clarification or add additional context in comments.

4 Comments

That's a very useful utility, it solved the issue nicely. I'm surprised there wasn't something already included with JSONEncoder . Perhaps I've been spoilt by Newtonsoft.Json. I'll have to fix the Int/Double abiguity though.
I've been working on this a bit more today (see github); what Int/Double ambiguity do you need to fix? I've done some work there. I'm now storing the value internally as an NSNumber, which helps.
I just found the code through your RNJSON link. I have recently answered a similar question here and I think some of the code could be definitely simplified. However, unlike me, you are solving null cases which I ignored.
Is it also possible to decode it again, i.e. MyClass: Decodable?
1

You could use something like this:

extension MyClass {
    func jsonFormatted() throws -> String? {
        guard let data = data.data(using: .utf8) else {
            return nil
        }
        let anyData = try JSONSerialization.jsonObject(with: data, options: [])
        let dictionary = ["name": name, "data": anyData] as [String : Any]
        let jsonData = try JSONSerialization.data(withJSONObject: dictionary, options: .prettyPrinted)
        let jsonString = String(data: jsonData, encoding: .utf8)
        return jsonString
    }
}

So basically, you leave the structure of data intact, but the rest is wrapped in a dictionary that can be converted to that json string you want to achieve.
Note you'll need to handle the optional and errors that could be thrown. You can use this to test:

if let jsonString = try? instance.jsonFormatted() {
    print(jsonString)
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.