Mastodon

JSON vs Property Lists, Revisited

In a previous post I wrote about How JSON compares to Apple property lists and the obstacles to converting data between them. That was a while ago but the post is still accurate, as far as it goes. But Swift changes the situation in some ways, so an update is in order.

Recap: JSON vs. Property Lists

The previous post was motivated by the problems some people encountered trying to download JSON from a server and then save it as a property list. Converting between JSON and property lists often works but can fail due to conflicts in data types.

  • JSON often includes null values for keys. These would be translated to NSNull when parsing JSON, which would mean the data wasn’t a valid property list.
  • Property lists can include dates, base64-encoded data, and floating point numbers where the value is ±infinity or “not a number”. None of these can exist in JSON without converting the data somehow, only Foundation didn’t provide any built-in conversions.

But, why?

Why convert JSON to a property list? It’s a good question, and more relevant now than when I wrote the original post. JSON support has drastically improved, as I’ll discuss below. It’s reasonable to keep JSON data as JSON. But property lists are still ubiquitous in iOS and Mac development, so conversion still happens sometimes. I’m hoping that this post can help smooth out the bumps when converting is needed.

Revisting [NS]JSONSerialization

If you’re using Objective-C, NSJSONSerialization hasn’t changed. Go see my previous post if you’re having trouble converting to or from a property list.

If you’re using Swift, then JSONSerialization includes a slight change in behavior, related to differences in how Swift dictionaries work compared to Objective-C. Depending on your code, a JSON null might be represented by a doubly-wrapped nil value in the resulting dictionary. That’s something Swift dictionaries allow but Objective-C does not.

Whether that’s what you get depends on whether you allow nil entries, that is, whether your dictionary is able to hold optional values. Suppose you have this trivial JSON:

{
    "date": "2020-02-26T03:56:56Z",
    "thingThatMightBeNull": null
}

Let’s try converting it with JSONSerialization in two slightly different ways.

First, try allowing optional values:

let json = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String:Any?],

Since nil values are allowed in the converted dictionary, the type of json["thingThatMightBeNull"] is Optional<Optional<Any>>.Type, a doubly-wrapped optional. There are various ways to handle those. I prefer looking for what I expect by doing something like

if let thing = json["thingThatMightBeNull"] as? String { ... }

You don’t have to allow optionals. If you don’t, the result is slightly different. If you do this:

let json = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String:Any]

This doesn’t allow optionals, but the conversion still works. This time the type of json["thingThatMightBeNull"] is Optional<Any>.Type. If you unwrap it, you’ll get our old friend NSNull. That’s not nil, so it’s OK for a structure that can’t contain nil values.

If you’re aiming for a property list though, it doesn’t matter which you use. Neither version is valid property list data, so the handy write(toFile:, atomically:) function will fail. 😭

The Rise of Skywalker Codable

Swift gives us a different approach, thanks to Codable. If you declare a type for your data– a struct or whatever– instead of using a dictionary directly, you can bypass the problem by using JSONDecoder to parse the JSON, followed by using PropertyListEncoder to create the property list. A JSON null ends up as a nil value for one of your struct properties. The property list leaves that property out. So both formats handle the missing value as expected even though they have different ideas about nil/null values.

Using the JSON from above, you might define this structure:

struct MyStruct: Codable {
    var date: String?
    var thingThatMightBeNull: String?
}

Parsing the JSON is straightforward with JSONDecoder. As expected, date contains a string and thingThatMightBeNull is nil. Converting that to a property list is nearly as simple:

let plistEncoder = PropertyListEncoder()
plistEncoder.outputFormat = .xml
guard let encodedPlistData = try? plistEncoder.encode(myThing),
    let encodedPlistString = String(data:encodedPlistData, encoding: .utf8) else {
    return
}

At this point encodedPlistString looks like this. There’s no value for thingThatMightBeNull since that’s how nil values are usually handled with property lists.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>date</key>
	<string>2020-02-26T03:56:56Z</string>
</dict>
</plist>

Other data types

Some of you will have noticed that the date property was declared as a String?. Why not Date? Why not indeed. JSONDecoder supports a some built-in data conversions, which can give you more meaningful data and– since it’s the topic of this post– lead to more meaningful property lists. If I redefine the struct above like this:

struct MyStruct: Codable {
    var date: Date?
    var thingThatMightBeNull: String?
}

I can then decode the JSON above by setting the decoder’s dateDecodingStrategy to .iso8601. Converting to a property list uses exactly the same code but now the result looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>date</key>
	<date>2020-02-26T03:56:56Z</date>
</dict>
</plist>

It’s almost the same, but now the value is a <date> element instead of a <string>.

Other converters are available for binary data and the “non conforming” floating point values mentioned above.

The same conversions are available in the other direction. If you’re converting from a property list to JSON, it used to be that dates and binary data could require a bunch of extra work. With the built-in conversions though, the framework almost certainly has you covered.

No really, why?

If you’re working in Swift, as most iOS developers seem to be today, it’s a fair question of why you’d convert from JSON to a property list. With JSONEncoder you can easily convert Codable data structures to Data and keep them as JSON. You’re still likely to encounter property lists though, so it can be handy to know how to deal with them.