Support embedding JSON-like objects in swift code

JSON is a standard, popular, structured data format. The fact that JSON objects cannot be directly expressed in swift source code is a detriment, as it requires run-time evaluation. The compiler should be able to produce an object (probably of [String:Any]) at compile time. Both JavaScript and Python can take data structures that are JSON (or in the case of Python, sufficiently close as to not require reformatting). Additionally, extending past JSON, into JS or Python, defining a lamba function would be extremely useful.

For example in JS:

x = {
    "func": function (a,b) {
        return a+b
    }
}
x["func"](1,2) -> 3

Or in Python:

x =  {
        "func": lambda a,b: a+b
    }
x["func"](1,2) -> 3

Or in swift:

x = {
    "func": func (a,b) -> Int {
        return a+b
    }
}
x["func"](1,2) -> 3

This suggestion does not extend itself to how the code would referenced, the above examples are just the most plausible.

Why?

7 Likes

Swift already allows this, albeit not in precisely a JSON format. It also already supports the lambda functions you referenced:

let x: [String: Any] = [
	"func": { (a: Int, b: Int) -> Int in
		return a + b
	},
	"value": 231
]

let f = x["func"] as! (Int, Int) -> Int
print(f(1, 2)) // 3
2 Likes

The dynamicMemberLookup attribute was actually added in Swift with the motivation of working with dynamically-typed languages (such as Python and Javascript).

Though when using Any as a dynamic member result type, you lose type-inference (you still need to typecast with as? or as! each time). And with string-based keys, you lose autocompletion.

Also, there's ExpressibleByDictionaryLiteral, for when you want to add JSON-like initialization to existing types.

1 Like

What is your motivation? Are you looking to be able to copy JSON and paste it directly into Swift? Are you generating Swift source code with embedded data from a JSON source? Do you just prefer JavaScript’s use of symbols to Swift’s?

Both JavaScript and Python are dynamically typed.

Yes, being able to speak the data lingua franca of the internet transparently is a huge benefit. I am currently maintaining a multi-platform product, and I could just copy and paste these in on the other platforms, but with swift I have to do a lot of additional syntax conversion, which means now it has to be maintained separately.

I have a table large table that maps to values and functions, and while this is do-able in Swift It's a big table that now has to be maintained separately. I can't just encode it because it contains functions (so it is not true JSON) but I the lambdas simple enough to work on all platforms.

I was hoping Swift would allow { } for dictionaries (a shorthand for [String:Any]?) which would accomplish a lot of the syntax conversion that I am doing, though not entirely.

But as someone pointed out above (by providing the Swift version of the lambda) , the conversion of the lambda syntax is not portable.

So I guess this is a dumb idea.

But your responses have clued me into some areas I need to learn more. So it's not a total waste. :slight_smile:

If the whole table also have the same function type, I'd further suggest that they're typed.

let x: [String: (Int, Int) -> Int] = [
	"func": { $0 + $1 },
]

print(x["func"]!(1, 2)) // 3

Since that'd also reduce a good amount of pain point when trying to use untyped structure (JSON-like) in a typed language (Swift) as mentioned above.

1 Like

Another pain point I am having is the code often does things like:

var obj = {"a":1, "b": {"b1": someObj.prop } } // JS

Usage:

socket.send( JSON.stringify(obj) } // JS

However I can't figure out how to single-line that in Swift. As far as I can tell these anonymous objects are not nearly as trivial to write?

Edit to clarify, with just one level, it is trivial, but with a nested object?

Wouldn't this just be a dictionary? ["a": 1, "b": ["b1": someObj.prop]]

A proper, statically typed version of this would be to create an Encodable type to express the data:

struct SomeValue: Encodable {
    let a: Int
    let b: SubValue

    struct SubValue: Encodable {
        let b1: Int
    }
}

Which you would then send as send(SomeValue(a: 1, b: .init(b1: someObj.prop))).

But you can use the unstructured dictionary if you want, you just lose all of the benefits of Swift static type system.

1 Like

Consider using tuples as a lightweight, one-time-use type. Strongly typed, and is probably what you're looking for.

FWIW, it may actually be easier to have parts of your application in Javascript, then port it over to Swift over time.

https://developer.apple.com/documentation/javascriptcore

1 Like

I agree with @brandon. Maybe you can do something like this:

var obj = (a: 1, b: (b1: someObj.prop))

No part in the dictionary has the same type, a is an Int, b is an object, and b.b1 is... something. It's totally not a job for Dictionary. It's a job for an instance, like class, struct, or non-nominal struct (tuple). I doubt any of them would gain new syntax just to match JSON, which already sounds like a non-goal.

You could also use a recursive enumeration as a JSON type.

enum JSON {
  indirect case object([String: JSON])
  indirect case array([JSON])
  case number(Double) // Technically not specified by RFC 8259, but close enough
  case string(String)
  case boolean(Bool)
  case null
}

extension JSON: ExpressibleByDictionaryLiteral {
  init(dictionaryLiteral elements: (String, JSON)...) {
    self = .object(.init(uniqueKeysWithValues: elements))
  }
}

extension JSON: ExpressibleByArrayLiteral {
  init(arrayLiteral elements: JSON...) {
    self = .array(elements)
  }
}

extension JSON: ExpressibleByFloatLiteral {
  init(floatLiteral value: FloatLiteralType) {
    self = .number(value)
  }
}

extension JSON: ExpressibleByIntegerLiteral {
  init(integerLiteral value: IntegerLiteralType) {
    self = .number(.init(value))
  }
}

extension JSON: ExpressibleByStringLiteral {
  init(stringLiteral value: StringLiteralType) {
    self = .string(value)
  }
}

extension JSON: ExpressibleByBooleanLiteral {
  init(booleanLiteral value: BooleanLiteralType) {
    self = .boolean(value)
  }
}

extension JSON: ExpressibleByNilLiteral {
  init(nilLiteral: ()) {
    self = .null
  }
}

That approach preserves as much type information as possible, while still complying with RFC 8259 in almost every respect.

4 Likes