diff --git a/src/lib/json/CMakeLists.txt b/src/lib/json/CMakeLists.txt
index 0f0739620097dc160dc18d69bad4b3bbe130cfb5..51a1f027a8cbcd9a832ea26c24eca52f0dc36fe1 100644
--- a/src/lib/json/CMakeLists.txt
+++ b/src/lib/json/CMakeLists.txt
@@ -4,3 +4,4 @@ set(lib_sources
 
 set_source_files_compile_flags(${lib_sources})
 add_library(json_path STATIC ${lib_sources})
+target_link_libraries(json_path misc)
diff --git a/src/lib/json/json.c b/src/lib/json/json.c
index 3a40bca1295b980c0ec3975a13f5afd4199e418e..430caf221d0beaecb1f1ac005a4a3a0fd644d9eb 100644
--- a/src/lib/json/json.c
+++ b/src/lib/json/json.c
@@ -30,11 +30,19 @@
  */
 
 #include "json.h"
+
+#include <assert.h>
 #include <ctype.h>
 #include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
 #include <unicode/uchar.h>
 #include <unicode/utf8.h>
+
 #include "trivia/util.h"
+#include "third_party/PMurHash.h"
 
 /**
  * Read a single symbol from a string starting from an offset.
@@ -243,3 +251,271 @@ json_lexer_next_token(struct json_lexer *lexer, struct json_token *token)
 		return json_parse_identifier(lexer, token);
 	}
 }
+
+/**
+ * Compare JSON token keys.
+ */
+static int
+json_token_cmp(const struct json_token *a, const struct json_token *b)
+{
+	if (a->parent != b->parent)
+		return a->parent - b->parent;
+	if (a->type != b->type)
+		return a->type - b->type;
+	int ret = 0;
+	if (a->type == JSON_TOKEN_STR) {
+		if (a->len != b->len)
+			return a->len - b->len;
+		ret = memcmp(a->str, b->str, a->len);
+	} else if (a->type == JSON_TOKEN_NUM) {
+		ret = a->num - b->num;
+	} else {
+		unreachable();
+	}
+	return ret;
+}
+
+#define MH_SOURCE 1
+#define mh_name _json
+#define mh_key_t struct json_token *
+#define mh_node_t struct json_token *
+#define mh_arg_t void *
+#define mh_hash(a, arg) ((*(a))->hash)
+#define mh_hash_key(a, arg) ((a)->hash)
+#define mh_cmp(a, b, arg) (json_token_cmp(*(a), *(b)))
+#define mh_cmp_key(a, b, arg) (json_token_cmp((a), *(b)))
+#include "salad/mhash.h"
+
+static const uint32_t hash_seed = 13U;
+
+/**
+ * Compute the hash value of a JSON token.
+ *
+ * See the comment to json_tree::hash for implementation details.
+ */
+static uint32_t
+json_token_hash(struct json_token *token)
+{
+	uint32_t h = token->parent->hash;
+	uint32_t carry = 0;
+	const void *data;
+	int data_size;
+	if (token->type == JSON_TOKEN_STR) {
+		data = token->str;
+		data_size = token->len;
+	} else if (token->type == JSON_TOKEN_NUM) {
+		data = &token->num;
+		data_size = sizeof(token->num);
+	} else {
+		unreachable();
+	}
+	PMurHash32_Process(&h, &carry, data, data_size);
+	return PMurHash32_Result(h, carry, data_size);
+}
+
+int
+json_tree_create(struct json_tree *tree)
+{
+	memset(tree, 0, sizeof(struct json_tree));
+	tree->root.hash = hash_seed;
+	tree->root.type = JSON_TOKEN_END;
+	tree->root.max_child_idx = -1;
+	tree->root.sibling_idx = -1;
+	tree->hash = mh_json_new();
+	return tree->hash == NULL ? -1 : 0;
+}
+
+static void
+json_token_destroy(struct json_token *token)
+{
+ 	assert(token->max_child_idx == -1);
+	assert(token->sibling_idx == -1);
+	free(token->children);
+	token->children = NULL;
+}
+
+void
+json_tree_destroy(struct json_tree *tree)
+{
+	json_token_destroy(&tree->root);
+	mh_json_delete(tree->hash);
+}
+
+struct json_token *
+json_tree_lookup_slowpath(struct json_tree *tree, struct json_token *parent,
+			  const struct json_token *token)
+{
+	assert(token->type == JSON_TOKEN_STR);
+	struct json_token key;
+	key.type = token->type;
+	key.str = token->str;
+	key.len = token->len;
+	key.parent = parent;
+	key.hash = json_token_hash(&key);
+	mh_int_t id = mh_json_find(tree->hash, &key, NULL);
+	if (id == mh_end(tree->hash))
+		return NULL;
+	struct json_token **entry = mh_json_node(tree->hash, id);
+	assert(entry != NULL && *entry != NULL);
+	return *entry;
+}
+
+int
+json_tree_add(struct json_tree *tree, struct json_token *parent,
+	      struct json_token *token)
+{
+	assert(json_tree_lookup(tree, parent, token) == NULL);
+	token->parent = parent;
+	token->children = NULL;
+	token->children_capacity = 0;
+	token->max_child_idx = -1;
+	token->hash = json_token_hash(token);
+	int insert_idx = token->type == JSON_TOKEN_NUM ?
+			 (int)token->num : parent->max_child_idx + 1;
+	/*
+	 * Dynamically grow the children array if necessary.
+	 */
+	if (insert_idx >= parent->children_capacity) {
+		int new_size = parent->children_capacity == 0 ?
+			       8 : 2 * parent->children_capacity;
+		while (insert_idx >= new_size)
+			new_size *= 2;
+		struct json_token **children = realloc(parent->children,
+						new_size * sizeof(void *));
+		if (children == NULL)
+			return -1; /* out of memory */
+		memset(children + parent->children_capacity, 0,
+		       (new_size - parent->children_capacity) * sizeof(void *));
+		parent->children = children;
+		parent->children_capacity = new_size;
+	}
+	/*
+	 * Insert the token into the hash (only for tokens representing
+	 * JSON map entries, see the comment to json_tree::hash).
+	 */
+	if (token->type == JSON_TOKEN_STR) {
+		mh_int_t id = mh_json_put(tree->hash,
+			(const struct json_token **)&token, NULL, NULL);
+		if (id == mh_end(tree->hash))
+			return -1; /* out of memory */
+	}
+	/*
+	 * Success, now we can insert the new token into its parent's
+	 * children array.
+	 */
+	assert(parent->children[insert_idx] == NULL);
+	parent->children[insert_idx] = token;
+	parent->max_child_idx = MAX(parent->max_child_idx, insert_idx);
+	token->sibling_idx = insert_idx;
+	assert(json_tree_lookup(tree, parent, token) == token);
+	return 0;
+}
+
+void
+json_tree_del(struct json_tree *tree, struct json_token *token)
+{
+	struct json_token *parent = token->parent;
+	assert(token->sibling_idx >= 0);
+	assert(parent->children[token->sibling_idx] == token);
+	assert(json_tree_lookup(tree, parent, token) == token);
+	/*
+	 * Clear the entry corresponding to this token in parent's
+	 * children array and update max_child_idx if necessary.
+	 */
+	parent->children[token->sibling_idx] = NULL;
+	token->sibling_idx = -1;
+	while (parent->max_child_idx >= 0 &&
+	       parent->children[parent->max_child_idx] == NULL)
+		parent->max_child_idx--;
+	/*
+	 * Remove the token from the hash (only for tokens representing
+	 * JSON map entries, see the comment to json_tree::hash).
+	 */
+	if (token->type == JSON_TOKEN_STR) {
+		mh_int_t id = mh_json_find(tree->hash, token, NULL);
+		assert(id != mh_end(tree->hash));
+		mh_json_del(tree->hash, id, NULL);
+	}
+	json_token_destroy(token);
+	assert(json_tree_lookup(tree, parent, token) == NULL);
+}
+
+struct json_token *
+json_tree_lookup_path(struct json_tree *tree, struct json_token *root,
+		      const char *path, int path_len, int index_base)
+{
+	int rc;
+	struct json_lexer lexer;
+	struct json_token token;
+	struct json_token *ret = root;
+	json_lexer_create(&lexer, path, path_len, index_base);
+	while ((rc = json_lexer_next_token(&lexer, &token)) == 0 &&
+	       token.type != JSON_TOKEN_END && ret != NULL) {
+		ret = json_tree_lookup(tree, ret, &token);
+	}
+	if (rc != 0 || token.type != JSON_TOKEN_END)
+		return NULL;
+	return ret;
+}
+
+/**
+ * Return the child of @parent following @pos or NULL if @pos
+ * points to the last child in the children array. If @pos is
+ * NULL, this function returns the first child.
+ */
+static struct json_token *
+json_tree_child_next(struct json_token *parent, struct json_token *pos)
+{
+	assert(pos == NULL || pos->parent == parent);
+	struct json_token **arr = parent->children;
+	if (arr == NULL)
+		return NULL;
+	int idx = pos != NULL ? pos->sibling_idx + 1 : 0;
+	while (idx <= parent->max_child_idx && arr[idx] == NULL)
+		idx++;
+	return idx <= parent->max_child_idx ? arr[idx] : NULL;
+}
+
+/**
+ * Return the leftmost descendant of the tree rooted at @pos
+ * or NULL if the tree is empty.
+ */
+static struct json_token *
+json_tree_leftmost(struct json_token *pos)
+{
+	struct json_token *last;
+	do {
+		last = pos;
+		pos = json_tree_child_next(pos, NULL);
+	} while (pos != NULL);
+	return last;
+}
+
+struct json_token *
+json_tree_preorder_next(struct json_token *root, struct json_token *pos)
+{
+	struct json_token *next = json_tree_child_next(pos, NULL);
+	if (next != NULL)
+		return next;
+	while (pos != root) {
+		next = json_tree_child_next(pos->parent, pos);
+		if (next != NULL)
+			return next;
+		pos = pos->parent;
+	}
+	return NULL;
+}
+
+struct json_token *
+json_tree_postorder_next(struct json_token *root, struct json_token *pos)
+{
+	struct json_token *next;
+	if (pos == NULL)
+		return json_tree_leftmost(root);
+	if (pos == root)
+		return NULL;
+	next = json_tree_child_next(pos->parent, pos);
+	if (next != NULL)
+		return json_tree_leftmost(next);
+	return pos->parent;
+}
diff --git a/src/lib/json/json.h b/src/lib/json/json.h
index ebb2f1e1f1ab3e49d261fa966f80b8dd24e2e752..ffdd7a90522dee18ac37e86f8acf950bc5d6b7e1 100644
--- a/src/lib/json/json.h
+++ b/src/lib/json/json.h
@@ -30,11 +30,18 @@
  * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  * SUCH DAMAGE.
  */
+#include <stddef.h>
+#include <stdint.h>
+#include "trivia/util.h"
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
+#ifndef typeof
+#define typeof __typeof__
+#endif
+
 /**
  * Lexer for JSON paths:
  * <field>, <.field>, <[123]>, <['field']> and their combinations.
@@ -66,6 +73,10 @@ enum json_token_type {
  * Element of a JSON path. It can be either string or number.
  * String idenfiers are in ["..."] and between dots. Numbers are
  * indexes in [...].
+ *
+ * May be organized in a tree-like structure reflecting a JSON
+ * document structure, for more details see the comment to struct
+ * json_tree.
  */
 struct json_token {
 	enum json_token_type type;
@@ -79,6 +90,118 @@ struct json_token {
 		/** Index value. */
 		int num;
 	};
+	/** Pointer to the parent token in a JSON tree. */
+	struct json_token *parent;
+	/**
+	 * Array of child tokens in a JSON tree. Indexes in this
+	 * array match [token.num] index for JSON_TOKEN_NUM type
+	 * and are allocated sequentially for JSON_TOKEN_STR child
+	 * tokens.
+	 */
+	struct json_token **children;
+	/** Allocation size of children array. */
+	int children_capacity;
+	/**
+	 * Max occupied index in the children array or -1 if
+	 * the children array is empty.
+	 */
+	int max_child_idx;
+	/**
+	 * Index of this token in parent's children array or -1
+	 * if the token was removed from a JSON tree or represents
+	 * a JSON tree root.
+	 */
+	int sibling_idx;
+	/**
+	 * Hash value of the token. Used for lookups in a JSON tree.
+	 * For more details, see the comment to json_tree::hash.
+	 */
+	uint32_t hash;
+};
+
+struct mh_json_t;
+
+/**
+ * This structure is used for organizing JSON tokens produced
+ * by a lexer in a tree-like structure reflecting a JSON document
+ * structure.
+ *
+ * Each intermediate node of the tree corresponds to either
+ * a JSON map or an array, depending on the key type used by
+ * its children (JSON_TOKEN_STR or JSON_TOKEN_NUM, respectively).
+ * Leaf nodes may represent both complex JSON structures and
+ * final values - it is not mandated by the JSON tree design.
+ * The root of the tree doesn't have a key and is preallocated
+ * when the tree is created.
+ *
+ * The json_token structure is intrusive by design, i.e. to store
+ * arbitrary information in a JSON tree, one has to incorporate it
+ * into a user defined structure.
+ *
+ * Example:
+ *
+ *   struct data {
+ *           ...
+ *           struct json_token token;
+ *   };
+ *
+ *   struct json_tree tree;
+ *   json_tree_create(&tree);
+ *   struct json_token *parent = &tree->root;
+ *
+ *   // Add a path to the tree.
+ *   struct data *data = data_new();
+ *   struct json_lexer lexer;
+ *   json_lexer_create(&lexer, path, path_len);
+ *   json_lexer_next_token(&lexer, &data->token);
+ *   while (data->token.type != JSON_TOKEN_END) {
+ *           json_tree_add(&tree, parent, &data->token);
+ *           parent = &data->token;
+ *           data = data_new();
+ *           json_lexer_next_token(&lexer, &data->token);
+ *   }
+ *   data_delete(data);
+ *
+ *   // Look up a path in the tree.
+ *   data = json_tree_lookup_path(&tree, &tree.root,
+ *                                path, path_len);
+ */
+struct json_tree {
+	/**
+	 * Preallocated token corresponding to the JSON tree root.
+	 * It doesn't have a key (set to JSON_TOKEN_END).
+	 */
+	struct json_token root;
+	/**
+	 * Hash table that is used to quickly look up a token
+	 * corresponding to a JSON map item given a key and
+	 * a parent token. We store all tokens that have type
+	 * JSON_TOKEN_STR in this hash table. Apparently, we
+	 * don't need to store JSON_TOKEN_NUM tokens as we can
+	 * quickly look them up in the children array anyway.
+	 *
+	 * The hash table uses pair <parent, key> as key, so
+	 * even tokens that happen to have the same key will
+	 * have different keys in the hash. To look up a tree
+	 * node corresponding to a particular path, we split
+	 * the path into tokens and look up the first token
+	 * in the root node and each following token in the
+	 * node returned at the previous step.
+	 *
+	 * We compute a hash value for a token by hashing its
+	 * key using the hash value of its parent as seed. This
+	 * is equivalent to computing hash for the path leading
+	 * to the token. However, we don't need to recompute
+	 * hash starting from the root at each step as we
+	 * descend the tree looking for a specific path, and we
+	 * can start descent from any node, not only from the root.
+	 *
+	 * As a consequence of this hashing technique, even
+	 * though we don't need to store JSON_TOKEN_NUM tokens
+	 * in the hash table, we still have to compute hash
+	 * values for them.
+	 */
+	struct mh_json_t *hash;
 };
 
 /**
@@ -112,6 +235,217 @@ json_lexer_create(struct json_lexer *lexer, const char *src, int src_len,
 int
 json_lexer_next_token(struct json_lexer *lexer, struct json_token *token);
 
+/**
+ * Initialize a new empty JSON tree.
+ *
+ * Returns 0 on success, -1 on memory allocation error.
+ */
+int
+json_tree_create(struct json_tree *tree);
+
+/**
+ * Destroy a JSON tree.
+ *
+ * Note, this routine expects the tree to be empty - the caller
+ * is supposed to use json_tree_foreach_safe() and json_tree_del()
+ * to dismantle the tree before calling this function.
+ */
+void
+json_tree_destroy(struct json_tree *tree);
+
+/**
+ * Internal function, use json_tree_lookup() instead.
+ */
+struct json_token *
+json_tree_lookup_slowpath(struct json_tree *tree, struct json_token *parent,
+			  const struct json_token *token);
+
+/**
+ * Look up a token in a JSON tree given a parent token and a key.
+ *
+ * Returns NULL if not found.
+ */
+static inline struct json_token *
+json_tree_lookup(struct json_tree *tree, struct json_token *parent,
+		 const struct json_token *token)
+{
+	struct json_token *ret = NULL;
+	if (likely(token->type == JSON_TOKEN_NUM)) {
+		ret = (int)token->num < parent->children_capacity ?
+		      parent->children[token->num] : NULL;
+	} else {
+		ret = json_tree_lookup_slowpath(tree, parent, token);
+	}
+	return ret;
+}
+
+/**
+ * Insert a token into a JSON tree at a given position.
+ *
+ * The token key (json_token::type and num/str,len) must be set,
+ * e.g. by json_lexer_next_token(). The caller must ensure that
+ * no token with the same key is linked to the same parent, e.g.
+ * with json_tree_lookup().
+ *
+ * Returns 0 on success, -1 on memory allocation error.
+ */
+int
+json_tree_add(struct json_tree *tree, struct json_token *parent,
+	      struct json_token *token);
+
+/**
+ * Delete a token from a JSON tree.
+ *
+ * The token must be linked to the tree (see json_tree_add())
+ * and must not have any children.
+ */
+void
+json_tree_del(struct json_tree *tree, struct json_token *token);
+
+/**
+ * Look up a token in a JSON tree by path.
+ *
+ * The path is relative to the given root token. In order to
+ * look up a token by absolute path, pass json_tree::root.
+ * The index_base is passed to json_lexer used for tokenizing
+ * the path, see json_lexer_create() for more details.
+ *
+ * Returns NULL if no token is found or the path is invalid.
+ */
+struct json_token *
+json_tree_lookup_path(struct json_tree *tree, struct json_token *root,
+		      const char *path, int path_len, int index_base);
+
+/**
+ * Perform pre-order traversal in a JSON subtree rooted
+ * at a given node.
+ *
+ * To start a new traversal, pass NULL for @pos.
+ * Returns @root at the first iteration.
+ * Returns NULL when traversal is over.
+ */
+struct json_token *
+json_tree_preorder_next(struct json_token *root, struct json_token *pos);
+
+/**
+ * Perform post-order traversal in a JSON subtree rooted
+ * at a given node.
+ *
+ * To start a new traversal, pass NULL for @pos.
+ * Returns @root at the last iteration.
+ * Returns NULL when traversal is over.
+ */
+struct json_token *
+json_tree_postorder_next(struct json_token *root, struct json_token *pos);
+
+/**
+ * Perform pre-order JSON tree traversal.
+ * Note, this function does not visit the root node.
+ * See also json_tree_preorder_next().
+ */
+#define json_tree_foreach_preorder(pos, root)				     \
+	for ((pos) = json_tree_preorder_next((root), (root));		     \
+	     (pos) != NULL;						     \
+	     (pos) = json_tree_preorder_next((root), (pos)))
+
+/**
+ * Perform post-order JSON tree traversal.
+ * Note, this function does not visit the root node.
+ * See also json_tree_postorder_next().
+ */
+#define json_tree_foreach_postorder(pos, root)				     \
+	for ((pos) = json_tree_postorder_next((root), NULL);		     \
+	     (pos) != (root);						     \
+	     (pos) = json_tree_postorder_next((root), (pos)))
+
+/**
+ * Perform post-order JSON tree traversal safe against node removal.
+ * Note, this function does not visit the root node.
+ * See also json_tree_postorder_next().
+ */
+#define json_tree_foreach_safe(pos, root, tmp)				     \
+	for ((pos) = json_tree_postorder_next((root), NULL);		     \
+	     (pos) != (root) &&						     \
+	     ((tmp) = json_tree_postorder_next((root), (pos))) != NULL;	     \
+	     (pos) = (tmp))
+
+/**
+ * Return a container of a json_tree_token.
+ */
+#define json_tree_entry(token, type, member)				     \
+	container_of((token), type, member)
+
+/**
+ * Return a container of a json_tree_token or NULL if @token is NULL.
+ */
+#define json_tree_entry_safe(token, type, member)			     \
+	((token) != NULL ? json_tree_entry((token), type, member) : NULL)    \
+
+/**
+ * Container-aware wrapper around json_tree_lookup().
+ */
+#define json_tree_lookup_entry(tree, parent, token, type, member) ({	     \
+	struct json_token *ret = json_tree_lookup((tree), (parent), (token));\
+	json_tree_entry_safe(ret, type, member);			     \
+})
+
+/**
+ * Container-aware wrapper around json_tree_lookup_path().
+ */
+#define json_tree_lookup_path_entry(tree, root, path, path_len, index_base,  \
+				    type, member) ({			     \
+	struct json_token *ret = json_tree_lookup_path((tree), (root),	     \
+					(path), (path_len), (index_base));   \
+	json_tree_entry_safe(ret, type, member);			     \
+})
+
+/**
+ * Container-aware wrapper around json_tree_preorder_next().
+ */
+#define json_tree_preorder_next_entry(root, pos, type, member) ({	     \
+	struct json_token *next = json_tree_preorder_next((root), (pos));    \
+	json_tree_entry_safe(next, type, member);			     \
+})
+
+/**
+ * Container-aware wrapper around json_tree_postorder_next().
+ */
+#define json_tree_postorder_next_entry(root, pos, type, member) ({	     \
+	struct json_token *next = json_tree_postorder_next((root), (pos));   \
+	json_tree_entry_safe(next, type, member);			     \
+})
+
+/**
+ * Container-aware version of json_tree_foreach_preorder().
+ */
+#define json_tree_foreach_entry_preorder(pos, root, type, member)	     \
+	for ((pos) = json_tree_preorder_next_entry((root), (root),	     \
+						   type, member);	     \
+	     (pos) != NULL;						     \
+	     (pos) = json_tree_preorder_next_entry((root), &(pos)->member,   \
+						   type, member))
+
+/**
+ * Container-aware version of json_tree_foreach_postorder().
+ */
+#define json_tree_foreach_entry_postorder(pos, root, type, member)	     \
+	for ((pos) = json_tree_postorder_next_entry((root), NULL,	     \
+						    type, member);	     \
+	     &(pos)->member != (root);					     \
+	     (pos) = json_tree_postorder_next_entry((root), &(pos)->member,  \
+						     type, member))
+
+/**
+ * Container-aware version of json_tree_foreach_safe().
+ */
+#define json_tree_foreach_entry_safe(pos, root, type, member, tmp)	     \
+	for ((pos) = json_tree_postorder_next_entry((root), NULL,	     \
+						    type, member);	     \
+	     &(pos)->member != (root) &&				     \
+	     ((tmp) = json_tree_postorder_next_entry((root), &(pos)->member, \
+						     type, member)) != NULL; \
+	     (pos) = (tmp))
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/test/unit/json_path.c b/test/unit/json_path.c
index 954c745c862f8722fccf9fe6335b306cffe2e867..aa692e8a0a5314a210aa7ae7581539202c03c062 100644
--- a/test/unit/json_path.c
+++ b/test/unit/json_path.c
@@ -2,6 +2,7 @@
 #include "unit.h"
 #include "trivia/util.h"
 #include <string.h>
+#include <stdbool.h>
 
 #define INDEX_BASE 1
 
@@ -165,14 +166,253 @@ test_errors()
 	footer();
 }
 
+struct test_struct {
+	int value;
+	struct json_token node;
+};
+
+struct test_struct *
+test_struct_alloc(struct test_struct *records_pool, int *pool_idx)
+{
+	struct test_struct *ret = &records_pool[*pool_idx];
+	*pool_idx = *pool_idx + 1;
+	memset(&ret->node, 0, sizeof(ret->node));
+	return ret;
+}
+
+struct test_struct *
+test_add_path(struct json_tree *tree, const char *path, uint32_t path_len,
+	      struct test_struct *records_pool, int *pool_idx)
+{
+	int rc;
+	struct json_lexer lexer;
+	struct json_token *parent = &tree->root;
+	json_lexer_create(&lexer, path, path_len, INDEX_BASE);
+	struct test_struct *field = test_struct_alloc(records_pool, pool_idx);
+	while ((rc = json_lexer_next_token(&lexer, &field->node)) == 0 &&
+		field->node.type != JSON_TOKEN_END) {
+		struct json_token *next =
+			json_tree_lookup(tree, parent, &field->node);
+		if (next == NULL) {
+			rc = json_tree_add(tree, parent, &field->node);
+			fail_if(rc != 0);
+			next = &field->node;
+			field = test_struct_alloc(records_pool, pool_idx);
+		}
+		parent = next;
+	}
+	fail_if(rc != 0 || field->node.type != JSON_TOKEN_END);
+	/* Release field. */
+	*pool_idx = *pool_idx - 1;
+	return json_tree_entry(parent, struct test_struct, node);
+}
+
+void
+test_tree()
+{
+	header();
+	plan(54);
+
+	struct json_tree tree;
+	int rc = json_tree_create(&tree);
+	fail_if(rc != 0);
+
+	struct test_struct records[7];
+	for (int i = 0; i < 6; i++)
+		records[i].value = i;
+
+	const char *path1 = "[1][10]";
+	const char *path2 = "[1][20].file";
+	const char *path3 = "[1][20].file[2]";
+	const char *path4 = "[1][20].file[8]";
+	const char *path4_copy = "[1][20][\"file\"][8]";
+	const char *path_unregistered = "[1][3]";
+
+	int records_idx = 0;
+	struct test_struct *node, *node_tmp;
+	node = test_add_path(&tree, path1, strlen(path1), records,
+			     &records_idx);
+	is(node, &records[1], "add path '%s'", path1);
+
+	node = test_add_path(&tree, path2, strlen(path2), records,
+			     &records_idx);
+	is(node, &records[3], "add path '%s'", path2);
+
+	node = test_add_path(&tree, path3, strlen(path3), records,
+			     &records_idx);
+	is(node, &records[4], "add path '%s'", path3);
+
+	node = test_add_path(&tree, path4, strlen(path4), records,
+			     &records_idx);
+	is(node, &records[5], "add path '%s'", path4);
+
+	node = test_add_path(&tree, path4_copy, strlen(path4_copy), records,
+			     &records_idx);
+	is(node, &records[5], "add path '%s'", path4_copy);
+
+	node = json_tree_lookup_path_entry(&tree, &tree.root, path1,
+					   strlen(path1), INDEX_BASE,
+					   struct test_struct, node);
+	is(node, &records[1], "lookup path '%s'", path1);
+
+	node = json_tree_lookup_path_entry(&tree, &tree.root, path2,
+					   strlen(path2), INDEX_BASE,
+					   struct test_struct, node);
+	is(node, &records[3], "lookup path '%s'", path2);
+
+	node = json_tree_lookup_path_entry(&tree, &tree.root, path_unregistered,
+					   strlen(path_unregistered), INDEX_BASE,
+					   struct test_struct, node);
+	is(node, NULL, "lookup unregistered path '%s'", path_unregistered);
+
+	/* Test iterators. */
+	struct json_token *token = NULL, *tmp;
+	const struct json_token *tokens_preorder[] =
+		{&records[0].node, &records[1].node, &records[2].node,
+		 &records[3].node, &records[4].node, &records[5].node};
+	int cnt = sizeof(tokens_preorder)/sizeof(tokens_preorder[0]);
+	int idx = 0;
+
+	json_tree_foreach_preorder(token, &tree.root) {
+		if (idx >= cnt)
+			break;
+		struct test_struct *t1 =
+			json_tree_entry(token, struct test_struct, node);
+		struct test_struct *t2 =
+			json_tree_entry(tokens_preorder[idx],
+					struct test_struct, node);
+		is(token, tokens_preorder[idx],
+		   "test foreach pre order %d: have %d expected of %d",
+		   idx, t1->value, t2->value);
+		++idx;
+	}
+	is(idx, cnt, "records iterated count %d of %d", idx, cnt);
+
+	const struct json_token *tree_nodes_postorder[] =
+		{&records[1].node, &records[4].node, &records[5].node,
+		 &records[3].node, &records[2].node, &records[0].node};
+	cnt = sizeof(tree_nodes_postorder)/sizeof(tree_nodes_postorder[0]);
+	idx = 0;
+	json_tree_foreach_postorder(token, &tree.root) {
+		if (idx >= cnt)
+			break;
+		struct test_struct *t1 =
+			json_tree_entry(token, struct test_struct, node);
+		struct test_struct *t2 =
+			json_tree_entry(tree_nodes_postorder[idx],
+					struct test_struct, node);
+		is(token, tree_nodes_postorder[idx],
+		   "test foreach post order %d: have %d expected of %d",
+		   idx, t1->value, t2->value);
+		++idx;
+	}
+	is(idx, cnt, "records iterated count %d of %d", idx, cnt);
+
+	idx = 0;
+	json_tree_foreach_safe(token, &tree.root, tmp) {
+		if (idx >= cnt)
+			break;
+		struct test_struct *t1 =
+			json_tree_entry(token, struct test_struct, node);
+		struct test_struct *t2 =
+			json_tree_entry(tree_nodes_postorder[idx],
+					struct test_struct, node);
+		is(token, tree_nodes_postorder[idx],
+		   "test foreach safe order %d: have %d expected of %d",
+		   idx, t1->value, t2->value);
+		++idx;
+	}
+	is(idx, cnt, "records iterated count %d of %d", idx, cnt);
+
+	idx = 0;
+	json_tree_foreach_entry_preorder(node, &tree.root, struct test_struct,
+					 node) {
+		if (idx >= cnt)
+			break;
+		struct test_struct *t =
+			json_tree_entry(tokens_preorder[idx],
+					struct test_struct, node);
+		is(&node->node, tokens_preorder[idx],
+		   "test foreach entry pre order %d: have %d expected of %d",
+		   idx, node->value, t->value);
+		idx++;
+	}
+	is(idx, cnt, "records iterated count %d of %d", idx, cnt);
+
+	idx = 0;
+	json_tree_foreach_entry_postorder(node, &tree.root, struct test_struct,
+					  node) {
+		if (idx >= cnt)
+			break;
+		struct test_struct *t =
+			json_tree_entry(tree_nodes_postorder[idx],
+					struct test_struct, node);
+		is(&node->node, tree_nodes_postorder[idx],
+		   "test foreach entry post order %d: have %d expected of %d",
+		   idx, node->value, t->value);
+		idx++;
+	}
+	is(idx, cnt, "records iterated count %d of %d", idx, cnt);
+
+	/* Test record deletion. */
+	is(records[3].node.max_child_idx, 7, "max_child_index %d expected of %d",
+	   records[3].node.max_child_idx, 7);
+	json_tree_del(&tree, &records[5].node);
+	is(records[3].node.max_child_idx, 1, "max_child_index %d expected of %d",
+	   records[3].node.max_child_idx, 1);
+	json_tree_del(&tree, &records[4].node);
+	is(records[3].node.max_child_idx, -1, "max_child_index %d expected of %d",
+	   records[3].node.max_child_idx, -1);
+	node = json_tree_lookup_path_entry(&tree, &tree.root, path3,
+					   strlen(path3), INDEX_BASE,
+					   struct test_struct, node);
+	is(node, NULL, "lookup removed path '%s'", path3);
+
+	node = json_tree_lookup_path_entry(&tree, &tree.root, path4,
+					   strlen(path4), INDEX_BASE,
+					   struct test_struct, node);
+	is(node, NULL, "lookup removed path '%s'", path4);
+
+	node = json_tree_lookup_path_entry(&tree, &tree.root, path2,
+					   strlen(path2), INDEX_BASE,
+					   struct test_struct, node);
+	is(node, &records[3], "lookup path was not corrupted '%s'", path2);
+
+	const struct json_token *tree_nodes_postorder_new[] =
+		{&records[1].node, &records[3].node,
+		 &records[2].node, &records[0].node};
+	cnt = sizeof(tree_nodes_postorder_new) /
+	      sizeof(tree_nodes_postorder_new[0]);
+	idx = 0;
+	json_tree_foreach_entry_safe(node, &tree.root, struct test_struct,
+				     node, node_tmp) {
+		if (idx >= cnt)
+			break;
+		struct test_struct *t =
+			json_tree_entry(tree_nodes_postorder_new[idx],
+					struct test_struct, node);
+		is(&node->node, tree_nodes_postorder_new[idx],
+		   "test foreach entry safe order %d: have %d expected of %d",
+		   idx, node->value, t->value);
+		json_tree_del(&tree, &node->node);
+		idx++;
+	}
+	is(idx, cnt, "records iterated count %d of %d", idx, cnt);
+	json_tree_destroy(&tree);
+
+	check_plan();
+	footer();
+}
+
 int
 main()
 {
 	header();
-	plan(2);
+	plan(3);
 
 	test_basic();
 	test_errors();
+	test_tree();
 
 	int rc = check_plan();
 	footer();
diff --git a/test/unit/json_path.result b/test/unit/json_path.result
index ad6f07e5a97b371633e0bcb2b70c01d72a226e1c..ee23e70c1cd27486075fed7383fd32f30ca9de9c 100644
--- a/test/unit/json_path.result
+++ b/test/unit/json_path.result
@@ -1,5 +1,5 @@
 	*** main ***
-1..2
+1..3
 	*** test_basic ***
     1..71
     ok 1 - parse <[1]>
@@ -100,4 +100,62 @@ ok 1 - subtests
     ok 21 - invalid token for index_base 1
 ok 2 - subtests
 	*** test_errors: done ***
+	*** test_tree ***
+    1..54
+    ok 1 - add path '[1][10]'
+    ok 2 - add path '[1][20].file'
+    ok 3 - add path '[1][20].file[2]'
+    ok 4 - add path '[1][20].file[8]'
+    ok 5 - add path '[1][20]["file"][8]'
+    ok 6 - lookup path '[1][10]'
+    ok 7 - lookup path '[1][20].file'
+    ok 8 - lookup unregistered path '[1][3]'
+    ok 9 - test foreach pre order 0: have 0 expected of 0
+    ok 10 - test foreach pre order 1: have 1 expected of 1
+    ok 11 - test foreach pre order 2: have 2 expected of 2
+    ok 12 - test foreach pre order 3: have 3 expected of 3
+    ok 13 - test foreach pre order 4: have 4 expected of 4
+    ok 14 - test foreach pre order 5: have 5 expected of 5
+    ok 15 - records iterated count 6 of 6
+    ok 16 - test foreach post order 0: have 1 expected of 1
+    ok 17 - test foreach post order 1: have 4 expected of 4
+    ok 18 - test foreach post order 2: have 5 expected of 5
+    ok 19 - test foreach post order 3: have 3 expected of 3
+    ok 20 - test foreach post order 4: have 2 expected of 2
+    ok 21 - test foreach post order 5: have 0 expected of 0
+    ok 22 - records iterated count 6 of 6
+    ok 23 - test foreach safe order 0: have 1 expected of 1
+    ok 24 - test foreach safe order 1: have 4 expected of 4
+    ok 25 - test foreach safe order 2: have 5 expected of 5
+    ok 26 - test foreach safe order 3: have 3 expected of 3
+    ok 27 - test foreach safe order 4: have 2 expected of 2
+    ok 28 - test foreach safe order 5: have 0 expected of 0
+    ok 29 - records iterated count 6 of 6
+    ok 30 - test foreach entry pre order 0: have 0 expected of 0
+    ok 31 - test foreach entry pre order 1: have 1 expected of 1
+    ok 32 - test foreach entry pre order 2: have 2 expected of 2
+    ok 33 - test foreach entry pre order 3: have 3 expected of 3
+    ok 34 - test foreach entry pre order 4: have 4 expected of 4
+    ok 35 - test foreach entry pre order 5: have 5 expected of 5
+    ok 36 - records iterated count 6 of 6
+    ok 37 - test foreach entry post order 0: have 1 expected of 1
+    ok 38 - test foreach entry post order 1: have 4 expected of 4
+    ok 39 - test foreach entry post order 2: have 5 expected of 5
+    ok 40 - test foreach entry post order 3: have 3 expected of 3
+    ok 41 - test foreach entry post order 4: have 2 expected of 2
+    ok 42 - test foreach entry post order 5: have 0 expected of 0
+    ok 43 - records iterated count 6 of 6
+    ok 44 - max_child_index 7 expected of 7
+    ok 45 - max_child_index 1 expected of 1
+    ok 46 - max_child_index -1 expected of -1
+    ok 47 - lookup removed path '[1][20].file[2]'
+    ok 48 - lookup removed path '[1][20].file[8]'
+    ok 49 - lookup path was not corrupted '[1][20].file'
+    ok 50 - test foreach entry safe order 0: have 1 expected of 1
+    ok 51 - test foreach entry safe order 1: have 3 expected of 3
+    ok 52 - test foreach entry safe order 2: have 2 expected of 2
+    ok 53 - test foreach entry safe order 3: have 0 expected of 0
+    ok 54 - records iterated count 4 of 4
+ok 3 - subtests
+	*** test_tree: done ***
 	*** main: done ***