How can I easily see the JSON output from my objects that conform to the `Codable` Protocol
Asked Answered
L

2

7

I deal with lots of objects that I serialize/deserialize to JSON using the Codable protocol.

It isn't that hard to create a JSONEncoder, set it up to pretty-print, convert the object to JSON, and then convert that to a string, but seems like a lot of work. Is there a simple way to say "please show me the JSON output for this object?"

EDIT:

Say for example I have the following structures:

struct Foo: Codable {
    let string1: String?
    let string2: String?
    let date: Date
    let val: Int
    let aBar: Bar
}

struct Bar: Codable {
    let name: String
}

And say I've created a Foo object:

let aBar = Bar(name: "Fred")
let aFoo = Foo(string1: "string1", string2: "string2", date: Date(), val: 42, aBar: aBar)

I could print that with a half-dozen lines of custom code:

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
guard let data = try? encoder.encode(aFoo),
    let output = String(data: data, encoding: .utf8)
    else { fatalError( "Error converting \(aFoo) to JSON string") }
print("JSON string = \(output)")

Which would give the output:

JSON string = {
  "date" : 557547327.56354201,
  "aBar" : {
    "name" : "Fred"
  },
  "string1" : "string1",
  "val" : 42,
  "string2" : "string2"
}

I get tired of writing the same half-dozen lines of code each time I need it. Is there an easier way?

Licit answered 2/9, 2018 at 0:30 Comment(0)
C
8

I would recommend creating a static encoder so you don't create a new encoder every time you call that property:

extension JSONEncoder {
    static let shared = JSONEncoder()
    static let iso8601 = JSONEncoder(dateEncodingStrategy: .iso8601)
    static let iso8601PrettyPrinted = JSONEncoder(dateEncodingStrategy: .iso8601, outputFormatting: .prettyPrinted)
}

extension JSONEncoder {    
    convenience init(dateEncodingStrategy: DateEncodingStrategy,
                         outputFormatting: OutputFormatting = [],
                      keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys) {
        self.init()
        self.dateEncodingStrategy = dateEncodingStrategy
        self.outputFormatting = outputFormatting
        self.keyEncodingStrategy = keyEncodingStrategy
    }
}

Considering that you are calling this method inside a Encodable extension you can just force try!. You can also force the conversion from data to string:

extension Encodable {
    func data(using encoder: JSONEncoder = .iso8601) throws -> Data {
        try encoder.encode(self)
    }
    func dataPrettyPrinted() throws -> Data {
        try JSONEncoder.iso8601PrettyPrinted.encode(self)
    }
    // edit if you need the data using a custom date formatter
    func dataDateFormatted(with dateFormatter: DateFormatter) throws -> Data {
        JSONEncoder.shared.dateEncodingStrategy = .formatted(dateFormatter)
        return try JSONEncoder.shared.encode(self)
    }
    func json() throws -> String {
         String(data: try data(), encoding: .utf8) ?? ""
    }
    func jsonPrettyPrinted() throws -> String {
        String(data: try dataPrettyPrinted(), encoding: .utf8) ?? ""
    }
    func jsonDateFormatted(with dateFormatter: DateFormatter) throws -> String {
        return String(data: try dataDateFormatted(with: dateFormatter), encoding: .utf8) ?? ""
    }
}

Playground testing

struct Foo: Codable {
    let string1: String
    let string2: String
    let date: Date
    let val: Int
    let bar: Bar
}
struct Bar: Codable {
    let name: String
}

let bar = Bar(name: "Fred")
let foo = Foo(string1: "string1", string2: "string2", date: Date(), val: 42, bar: bar)

try! print("JSON\n=================\n", foo.json(), terminator: "\n\n")
try! print("JSONPrettyPrinted\n=================\n", foo.jsonPrettyPrinted(), terminator: "\n\n")
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
try! print("JSONDateFormatted\n=================\n", foo.jsonDateFormatted(with: dateFormatter))

This will print

JSON
=================
{"date":"2020-11-06T20:22:55Z","bar":{"name":"Fred"},"string1":"string1","val":42,"string2":"string2"}

JSONPrettyPrinted
=================
{
"date" : "2020-11-06T20:22:55Z",
"bar" : {
"name" : "Fred"
},
"string1" : "string1",
"val" : 42,
"string2" : "string2"
}

JSONDateFormatted
=================
{"date":"6 November 2020","bar":{"name":"Fred"},"string1":"string1","val":42,"string2":"string2"}

Chalmer answered 2/9, 2018 at 2:45 Comment(9)
How are you adding static vars to an extension? The compiler complains when I try that. Also, I don't see the initializer JSONEncoder .init(dateEncodingStrategy:outputFormatting:)Licit
@DuncanC I am using the latest AppStore Xcode version Swift 4.1Chalmer
Can you post your entire playground then?Licit
convenience initializerChalmer
dropbox.com/s/ijmay06gthc881f/…Chalmer
Nicely done. (Voted.) The only thing you didn't do is allow the user to specify a custom DateFormatter/date encoding strategy.Licit
Also, I thought classes didn't let you add static vars in extensions (As supported by this thread: #38863858). Is that new with Swift 4?Licit
@DuncanC I don't think it is worthy to allow passing a date formatter but you can easily make a method that uses a static formatter check my editChalmer
regarding static it is not allowed on Protocols but ok on structures and classes. Static stored properties not supported in protocol extensionsChalmer
L
7

There isn't a stock way to convert a Codable object graph to a "pretty" JSON string, but it's pretty easy to define a protocol to do it so you don't write the same conversion code over and over.

You can simply create an extension to the Encodable protocol, like this:

extension Encodable {
    var prettyJSON: String {
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        guard let data = try? encoder.encode(self),
            let output = String(data: data, encoding: .utf8)
            else { return "Error converting \(self) to JSON string" }
        return output
    }
}

Then for any JSON object

print(myJSONobject.prettyJSON)

and it displays the JSON text in "pretty printed" form.

One thing the above won't do is support custom formatting of dates. To do that we can modify prettyJSON to be a function rather than a computed property, where it takes an optional DateFormatter as a parameter with a default value of nil.

extension Encodable {
    func prettyJSON(formatter: DateFormatter? = nil) -> String {
        let encoder = JSONEncoder()
        if let formatter = formatter {
            encoder.dateEncodingStrategy = .formatted(formatter)
        }
        encoder.outputFormatting = .prettyPrinted
        guard let data = try? encoder.encode(self),
            let output = String(data: data, encoding: .utf8)
            else { return "Error converting \(self) to JSON string" }
        return output
    }
}

Then you can use it just like the above, except that you need to add parentheses after prettyJSON, e.g.

print(myJSONobject.prettyJSON())

That form ignores the new DateFormatter parameter, and will output the same JSON string as the above. However,If you have a custom date formatter:

var formatter = DateFormatter()
formatter.dateFormat = "MM-dd-yyyy HH:mm:ss"

print(myJSONobject.prettyJSON(formatter: formatter))

Then the dates in your object graph will be formatted using the specified DateFormatter

Licit answered 2/9, 2018 at 0:31 Comment(1)
It might be a good idea to throw instead of returning a String, so that doesn’t get passed to a backend..Jasisa

© 2022 - 2024 — McMap. All rights reserved.