Json standard values representation in stdlib

I recently had to code a swift code that had to parse part of a server response, and forward another part "as is" (because it could be anything, by design), and found myself having to use foundation types and JsonSerialization api.

Which led me to the realization that the swift stdlib, although implementing json parsing and encoding, didn't actually expose any of the types of the JSON standard anywhere, for users to use.
I actually like the codable mechanism for transparent json (de)serializing, but shouldn't a complete stdlib provide a full featured "json" module, that would include types of the Json standard ?

What kind of types are missing?
JSONSerialization use Swift equivalent when doing the coding, JSON string <-> NSString, JSON dict <-> NSDictionary, etc.

Or do you mean that the exact representation (like spacing, decimal representation) is loss when pass through a round of coding?

I guess @benjamin.g's question is how to write a Decodable type like this one capable of storing arbitrary JSON data (and also whether it should be part of the standard library):

enum JSONValue: Decodable {
  case int(Int)
  case float(Double)
  case string(String)
  case array([JSONValue])
  case object([String: JSONValue])

  init(decoder: Decoder) throws {
    // implementation goes here
  }
}

I went into a similar problem recently and don't yet have a solution. It'd be nice if each decoder exposed some kind of raw native type.

I wrote a JSONValue enum for working with unstructured JSON data. It looks pretty similar to the one @michelf posted above, but also has a none case to support null (which is a valid JSON value). It also has quite a few protocol conformances, including the relevant literal protocols which make it really convenient to construct ad-how JSON values when necessary.

I think it would be great to have a single robust implementation that the community can all use. This feels like something that might be better placed in Foundation than the standard library though.

1 Like

Indeed, I forgot the null case.

I think a fully decodable JSONValue would look somewhat like this. This approach to decoding seems a bit suboptimal though, which is why I think it'd be useful if each coder could provide its own native value type with optimized decoding.

enum JSONValue: Decodable {
  case null
  case bool(Bool)
  case int(Int)
  case float(Double)
  case string(String)
  case array([JSONValue])
  case object([String: JSONValue])

  init(from decoder: Decoder) throws {
	let value = try decoder.singleValueContainer()
	if value.decodeNil() {
		self = .null
		return
	} else if let bool = try? value.decode(Bool.self) {
		self = .bool(bool)
		return
	} else if let int = try? value.decode(Int.self) {
		self = .int(int)
		return
	} else if let float = try? value.decode(Double.self) {
		self = .float(float)
		return
	} else if let string = try? value.decode(String.self) {
		self = .string(string)
		return
	} else if let array = try? value.decode(Array<JSONValue>.self) {
		self = .array(array)
		return
	} else if let object = try? value.decode(Dictionary<String, JSONValue>.self) {
		self = .object(object)
		return
	}
	throw DecodingError.valueNotFound(JSONValue.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "No matching type for JSON value"))
  }
}

Edit: added missing case for Bool. I knew I was still missing something, Thank you @spevans.

According to https://www.json.org/json-en.html true and false are also valid so you need to add a case bool(Bool) as well

Btw, we've used this approach, and it works extremely slow. A lot of time spent in that long if else statement, and also in objc dynamic type checking.

My guess is some kind of code inside JSONDecoder already has a representation of the kind, somewhere. Maybe it's a transient value, but it has to go from bytes to string to json type (dictionary or array, number or string), then to custom struct type. I don't see how it would be able to parse a json stream otherwise.
And my guess is also that this should be performant. Now maybe it's all C inside, i don't know, but then maybe this would be a good opportunity to redevelop JsonDecoder entirely in swift from bytes to struct.

The last time I’ve dig in this (about 2 years ago) there was just call to NSJSONSerialization and dynamic typecheck to extract values. While NSJSONSerialization is very efficient, dynamic typechecks and conversion to native swift types are killing that efficiency.

Some floating point values will decode as int. Then there's Date and URL.

Also, there is no floating point bias in JSON. Use Decimal to represent numbers.