DynamicParser to reliably and reversibly convert JSON to structs
authorAlexey Spiridonov <lesha@fb.com>
Thu, 7 Apr 2016 16:53:34 +0000 (09:53 -0700)
committerFacebook Github Bot 1 <facebook-github-bot-1-bot@fb.com>
Thu, 7 Apr 2016 17:05:36 +0000 (10:05 -0700)
commit4785dfe0af27d00755cac61bacec34539bc9878c
tree336d45ff6a75f78c4d6b51422e846b6dffdfb749
parent2a196d5a008f42da5bdf009d204f81593c2adaa4
DynamicParser to reliably and reversibly convert JSON to structs

Summary:We have a bunch of code that manually parses `folly::dynamic`s into program structures. I can be quite hard to get this parsing to be good, user-friendly, and concise. This diff was primarily motivated by the mass of JSON-parsing done by Bistro, but this pattern recurs in other pieces of internal code that parse dynamics.

This diff **not** meant to replace using Thrift structs with Thrift's JSON serialization / deserialization. When all you have to deal with is correct, structured plain-old-data objects produced by another program -- **not** manually entered user input -- Thrift + JSON is perfect. Go use that.

However, sometimes you need to parse human-edited configuration. The input JSON might have complex semantics, and require validation beyond type-checking. The UI for editing your configs can easily enforce correct JSON syntax. Perhaps, you can use `folly/experimental/JSONSchema.h` to have your edit UI provide type correctness. Despite all this, people can still make semantic errors, and those can be impossible to detect until you interpret the config at runtime. Also, as your system evolves, sometimes you need to break semantic backwards-compatibility for the sake of moving forward ? thus making previously valid configurations invalid, and requiring them to be fixed up manually.

So, people end up needing to write manual parsers for `dynamic`s. These all have very similar recurring issues:

 - Verbose: to get an int field out of an object, typical code: (i) tests if the field is present, (ii) checks if the field is an integer, (iii) extracts the integer. Sometimes, you also want to handle exceptions, and compose helpful error messages. This makes the code far longer than its intent, and encourages people to write bad parsers.

 - Unsystematic: sometimes, we use `if (const auto* p = dyn_obj.get_ptr("key")) { ... }`, other times we use `dyn_obj.getDefault()` or `if (dyn_obj.count())`, and so on. The patterns differ subtly in meaning. Exceptions sometimes get thrown, leading to error messages that cannot be understood by the user.

 - Imperative parses: a typical parse proceeds step by step, and throws at the earliest error. This is bad because (i) errors have to be fixed one-by-one, instead of getting a full list upfront, (ii) even if 99% of the config is parseable, the imperative code has no way of recording the information it would have parsed after the first error.

 `DynamicParser` fixes all of the above, and makes your parsing so clean that you might not even bother with `JSONSchema` as your first line of defense -- type-coercing, type-enforcing, friendly-error-generating C++ ends up being more concise. Besides all the sweet syntax sugar, `DynamicParser` lets you parse **all** the valid data in your config, while recording  *all* the errors in a way that does not lose the original, buggy config. This means your code can parse a config that has errors, and still be able to meaningfully export it back to JSON. As a result, stateless clients (think REST APIs) can provide a far better user experience than just discarding the user?s input, and returning a cryptic error message.

For the details, read the docs (and see the example) in `DynamicParser.h`. Here are the principles of `DynamicParser::RECORD` mode in a nutshell:
 - Pre-populate your program struct with meaningful defaults **before** you parse.
 - Any config part that fails to parse will keep the default.
 - Any config part that parses successfully will get to update the program struct.
 - Any errors will be recorded with a helpful error message, the portion of the dynamic that caused the error, and the path through the dynamic to that portion.

 I ported Bistro to use this in D3136954. I looked at using this for JSONSchema's parsing of schemas, but it seemed like too much trouble for the gain, since it would require major surgery on the code.

Reviewed By: yfeldblum

Differential Revision: D2906819

fb-gh-sync-id: aa997b0399b17725f38712111715191ffe7f27aa
fbshipit-source-id: aa997b0399b17725f38712111715191ffe7f27aa
folly/Makefile.am
folly/experimental/DynamicParser-inl.h [new file with mode: 0644]
folly/experimental/DynamicParser.cpp [new file with mode: 0644]
folly/experimental/DynamicParser.h [new file with mode: 0644]
folly/experimental/test/DynamicParserTest.cpp [new file with mode: 0644]