4 ms·
You're probably already aware of this, but Haskell's JSON parser (named Aeson) does exactly what you ask for. In particular, it can be operated in two modes: 1
by tel 12y ago
You're probably already aware of this, but Haskell's JSON parser (named Aeson) does exactly what you ask for. In particular, it can be operated in two modes:
1. Untyped mode. Here we parse out values of type, amusingly, Value which reflect the total, untyped structure of JSON. We can then express queries deep inside of that structure and see what get's pulled out. For instance, using the `lens-aeson` package
v :: Value
x :: Maybe Integer -- Maybe means it might fail
x = v ^? key "foo"
. key "bar"
. key "baz"
. ix 3
. _Integer -- looks a bit like XPath, right?
2. Typed mode. Now we suggest to Aeson that some of the types of our program have direct translation into or from JSON (or both) and let the parser produce these translations in a type safe way. For instance, should we have a type like
data Point = Point { x :: Double, y :: Double }
then we can introduce a translation like
instance FromJSON Point where
parseJSON = withObject "Point" $ \o ->
Point <$> o .: "x"
<*> o .: "y"
which indicates that JSON objects of a syntax like `{"x": 1, "y": 1}` can be parsed into `Point`s. Technically, the parser designed there is minimal in that JSON objects with more fields can still be properly parsed, although that can be fixed.
Finally, you can easily anoint your untyped mode XPath-like queries with typed fragments. This is especially powerful if your typed fragments include untyped JSON Value fragments inside of them. It means that we can use lenses to dive into an untyped JSON Value, parse some fragment of it into a Point in a type-sensitive fashion, then dive further into that Point. If Points contained more JSON values then that "further dive" could be untyped or typed again.
- mikeash 12y agoThanks, I wasn't actually aware of any of this. It's cool to see how other languages handle this stuff. Better JSON parsing has been a constant thing of mine and I'm hoping to build some good tooling for Swift once it stabilizes, so it's great to get ideas from other stuff that's out there.
- eridius 12y agoRust has a good approach here as well, that's related to Haskell's approach. In Rust, a type can implement the traits `Encodable` and `Decodable`, which are general traits that represent the ability to encode to and decode from some arbitrary format, using objects of type Encoder and Decoder. Each supported format then provides implementations of Encoder / Decoder. The benefit of this approach is the Encodable/Decodable implementations can be automatically derived in a lot of cases. For JSON specifically, the serialize::json module provides Encoder/Decoder implementations, as well as a streaming JSON parser type that can be used to consume JSON outside of the Encodable/Decodable system, and a Builder object that consumes the streaming parser to produce a generic untyped "Json" value that represents the entire structure of the Json. And of course it has some convenience functions that simplify common operations like encoding a value into a string. Given all this, the equivalent of the Haskell operations looks like: let v: Json = ...; // get the Json value // the following traverses the Json to extract the same path as the Haskell // and treats the result as a 64-bit integer. // note: this API is slightly old and has some unnecessary stuff in here, // such as the find method taking allocated strings instead of slices. let x: Option<i64> = v.find_path([&"foo".to_string(), &"bar".to_string(), &"baz".to_string()]) // this should eventually become .find_path(["foo", "bar", "baz"]) .and_then(|obj| obj.as_list()) .and_then(|l| l.as_slice().get(3)) // the .as_slice() will go away .and_then(|val| val.as_i64()); There's talk of adding support for `do` sugar which would make this much simpler as well. The typed version is much simpler. We can define the struct with a derived Decodable like so: #[deriving(Decodable)] struct Point { x: f64, y: f64 } And then actually decode some Json like let x: json::DecodeResult<Point> = json::decode(source_str); // DecodeResult contains either the result, or an error.
- tel 12y agoTo be clear, you can autoderive the JSON encoding for Aeson, too. I just wanted to demonstrate the full thing. Also, the XPath implementation I noted is special over Rust's because it forms a Lens (Prism, really) so you can get and set at arbitrary positions or even sequences of positions: v & ix 3 . each . _Integer %~ succ The above increments each integer in the array stored at index 3 of `v` if one exists.
- riffraff 12y agopardon my ignorance, but in the last line, how does json::decode knows it should decode to a json::DecodeResult<Point> rather than a json::DecodeResult<Whatever> ?
- eridius 12y agoBecause of the type annotation on the `x` declaration. Rust takes type constraints on the result type into account when determining what implementation to call.
- thinkpad20 12y agoI really wish that Aeson's lookups and fromJSON returned Eithers, and not Maybes. If you try to parse a deeply nested structure represented in JSON, and just get a Nothing back, it really doesn't help you very much. Having an Either is much more helpful. (If anyone is aware of a canonical library for this, let me know. I wrote my own JSON library which has this functionality but it's not particularly robust or performant).
- taylorfausak 12y agoYou can use eitherDecode to get that behavior. http://hackage.haskell.org/package/aeson-0.8.0.0/docs/Data-Aeson.html#v:eitherDecode http://hackage.haskell.org/package/aeson-0.8.0.0/docs/Data-A...
- koomi 12y agoAeson's «fromJSON» returns a «Result» which is basically «Either String». Most other functions operate on «Parser», which also includes an error message if things go wrong. The only functions returning «Maybe» are «decode» and friends, and those have «Either String» variants. You can use «modifyFailure» and «typeMismatch» (and the «with» wrappers) to get more descriptive messages, without a bit of help they are quite useless.
- riffraff 12y agoFWIW, even Java allows something similar to 2, i.e. with Jackson public class Point { public String x, y; } Point v = new ObjectMapper().readValue(io, Point.class); It also works ok with Lists and Maps.