Search code examples
hhvmhacklang

Deserialize JSON in Hack strict mode


I have a nested JSON file, consisting of keys and values which are string only. But the structure of the JSON file is not fixed, so sometimes it could be nested 3 levels, sometimes only 2 levels. I wonder how i could serialize this in strict mode?

  "live" : {
"host" : "localhost",
"somevalue" : "nothing",
"anobject" : {
  "one" : "two",
  "three" : "four",
  "five" : {
    "six" : "seven"
  }
}

}

If i would know the structure of the JSON, i simply would write my own class for it, but since the keys are not fixed, and also the nesting could be into several levels, i really wonder how i cut put such an object into a specific type.

Any help or hints appreciated


Solution

  • I think invariants will serve you well here. First off, it might be helpful to know that you can type a keyed tree strictly in Hack:

    <?hh // strict
    class KeyedTree<+Tk as arraykey, +T> {
      public function __construct(
        private Map<Tk, KeyedTree<Tk, T>> $descendants = Map{},
        private ?T $v = null
      ) {}
    }
    

    (It must be a class because cyclic shape definitions are sadly not allowed)

    I haven't tried it yet, but type_structures and Fred Emmott's TypeAssert look to also be of interest. If some part of your JSON blob is known to be fixed, then you could isolate the nested, uncertain part and build a tree out of it with invariants. In the limiting case where the whole blob is unknown, then you could excise the TypeAssert since there's no interesting fixed structure to assert:

    use FredEmmott\TypeAssert\TypeAssert;
    class JSONParser {
        const type Blob = shape(
            'live' => shape(
                'host' => string, // fixed
                'somevalue' => string, // fixed
                'anobject' => KeyedTree<arraykey, mixed> // nested and uncertain
            )
        );
        public static function parse_json(string $json_str): this::Blob {
            $json = json_decode($json_str, true);
            invariant(!array_key_exists('anobject', $json), 'JSON is not properly formatted.');
            $json['anobject'] = self::DFS($json['anobject']);
              // replace the uncertain array with a `KeyedTree`
            return TypeAssert::matchesTypeStructure(
                type_structure(self::class, 'Blob'),
                $json
            );
            return $json;
        }
        public static function DFS(array<arraykey, mixed> $tree): KeyedTree<arraykey, mixed> {
            $descendants = Map{};
            foreach($tree as $k => $v) {
                if(is_array($v))
                    $descendants[$k] = self::DFS($v);
                else
                    $descendants[$k] = new KeyedTree(Map{}, $v); // leaf node
            }
            return new KeyedTree($descendants);
        }
    }
    

    Down the road, you'll still have to supplement containsKey invariants on the KeyedTree, but that's the reality with unstructured data in Hack.