0

I'm trying to add a JSONType protocol with no requirements that can be implemented by any type that can be passed to JSONSerialization.data(withJSONObject:) so that I know at compile time that the passed argument is a valid JSON object.

I'm facing the following problem with this code:

public protocol JSONType {
}

extension String: JSONType {
}

extension Array: JSONType where Element: JSONType {
}

func serializeToJSON(_ object: JSONType) -> Data {
    return try! JSONSerialization.data(withJSONObject: object)
}

var array1: [JSONType] = [""]
var array2: [JSONType] = [""]
array1[0] = array2 // compile error: Value of type '[JSONType]' does not conform to 'JSONType' in subscript assignment

To me it seems that the Array extension does just what the compiler is complaining to be missing. Is there a workaround or a more correct solution?

EDIT 1: when setting extension Array: JSONType where Element == JSONType as suggested in this comment, the code above compiles, but then the following doesn't:

public protocol JSONType {
}

extension String: JSONType {
}

extension Array: JSONType where Element == JSONType {
}

extension Dictionary: JSONType where Key == String, Value == JSONType {
}

var dict = [String: JSONType]()
dict[""] = [[String: JSONType]]() // compile error: Generic struct 'Array' requires the types '[String : JSONType]' and 'JSONType' be equivalent

... which then compiles again when resetting to extension Array: JSONType where Element: JSONType.

EDIT 2: I tried using JSONEncoder, but this gives similar compiler errors:

func serializeToJSON<T>(_ object: T) -> Data where T: Encodable {
    return try! JSONEncoder().encode(object)
}

var dict = [String: Encodable]()
dict[""] = [[String: Encodable]]() // compile error: Cannot assign value of type '[[String : Encodable]]' to subscript of type 'Encodable?'

var array1: [Encodable] = [""]
var array2: [Encodable] = [""]
array1[0] = array2 // compile error: Value of type '[Encodable]' does not conform to 'Encodable' in subscript assignment

serializeToJSON(dict) // compile error: Protocol 'Encodable' as a type cannot conform to the protocol itself
serializeToJSON(array1) // compile error: Protocol 'Encodable' as a type cannot conform to the protocol itself

When using Any instead of Encodable the compiler says Protocol 'Any' as a type cannot conform to 'Encodable'.

Nickkk
  • 2,261
  • 1
  • 25
  • 34
  • (sorry for the churn on Sweeper's original dupe close. They were right. Just make sure to read down to Hamish's answer. My answer is accepted, but Hamish goes much more into detail and explains how to fix this. You just need to add a `where Element == JSONType` extension too.) – Rob Napier Sep 02 '21 at 13:23
  • Thanks for the hint. I'm not sure if that answer can help in this case, so I've edited the question to add a case where your suggestion doesn't work. – Nickkk Sep 03 '21 at 12:01
  • `[String: JSONType]` is not `JSONType`, so this is not `[JSONType]`. (Conforming is not the same thing as ==.) You can't mix protocols this way. They do not conform to themselves, and you cannot conform types in multiple ways (so you can't have both `==` and `:` versions of this conformance, which you would require for what you're describing.) Don't go down this road. Just use JSONEncoder. That's what it's for. – Rob Napier Sep 03 '21 at 13:53
  • Note that in the future it will almost certainly be possible to build this (see https://github.com/apple/swift-evolution/blob/main/proposals/0309-unlock-existential-types-for-all-protocols.md for the current step on that journey), but you still probably shouldn't. Unless you have a very special case, the correct tool for this is JSONEncoder and generics rather than existentials. If you have a case where JSONEncoder doesn't work, post that question. (There are legitimate cases where it doesn't, but JSONSerialization is almost never the correct solution to those.) – Rob Napier Sep 03 '21 at 13:59
  • Thanks again. I had already considered using `JSONEncoder`, but it also doesn't work. I edited the question to add that case. Please let me know if you think this new part is enough to move to a new question. – Nickkk Sep 04 '21 at 09:01
  • The point of JSONEncoder is not to create these ad hoc `[String: JSONThing]` dictionaries. Make a struct that contains what you want and conform that to Encodable. Why do you need this dictionary? (There are some obscure uses for them, and it is possible to build things that handle it somewhat, but it is a lot of work when a simple struct solves the entire problem directly.) – Rob Napier Sep 04 '21 at 14:57
  • So when I say "use JSONEncoder" I didn't mean "swap JSONType for Encodable." That doesn't change anything at all (it has exactly the same limitations and headaches). I meant "use JSONEncoder in the standard way." If you need something more flexible for specialized purposes you can build things like this: https://stackoverflow.com/questions/65901928/swift-jsonencoder-encoding-class-containing-a-nested-raw-json-object-literal/65902852#65902852 (or my current experiments at https://github.com/rnapier/RNJSON). But start with just using structs or normal dictionaries (i.e. `[String: String]`) – Rob Napier Sep 04 '21 at 15:09
  • Thanks for posting those links, but I was looking to get compile-time JSON validity checks without having to adapt all the JSON data in the code. Currently I'm using `JSONSerialization.data(withJSONObject:)` with dictionaries with strings as keys and values that can be any supported JSON type, including nested dictionaries. Using `[String: String]` is too limiting in my case and since the JSON data is always different it would feel like an overkill to have many structs that are used only once. – Nickkk Sep 06 '21 at 16:13
  • See this version of RNJSON: https://github.com/rnapier/RNJSON/blob/0d12f5957f467b458272aed9e20df82affa85682/Sources/RNJSON/RNJSON.swift It handles `Any` and `[String: Any]` and will throw when there's an issue (rather than crashing like JSONSerialization), and offers "JSON-like" types to build arbitrary structures out of. There's no easy way to create directly what you're describing. – Rob Napier Sep 06 '21 at 16:18
  • Anything that looks like `[String: JSONProtocol]` is going to break because of Swift's limitations on how protocols work. Instead you want a "JSON" type that is easily bridged from other types (so you can do things like `let json: RNJSON = ["string": "value", "number": 1]` and it'll just magically "work" (which the linked code handles). – Rob Napier Sep 06 '21 at 16:25
  • Thanks, I very much appreciated your help. – Nickkk Oct 13 '21 at 11:48

0 Answers0