diff --git a/Documentation/Makefile.am b/Documentation/Makefile.am
index 51d186505b44..a168cec100c8 100644
--- a/Documentation/Makefile.am
+++ b/Documentation/Makefile.am
@@ -5,7 +5,8 @@ man1_MANS = \
 	ndctl-disable-region.1 \
 	ndctl-enable-namespace.1 \
 	ndctl-disable-namespace.1 \
-	ndctl-create-namespace.1
+	ndctl-create-namespace.1 \
+	ndctl-list.1
 
 XML_DEPS = \
 	$(top_srcdir)/version.m4 \
diff --git a/Documentation/ndctl-list.txt b/Documentation/ndctl-list.txt
new file mode 100644
index 000000000000..53847c56236e
--- /dev/null
+++ b/Documentation/ndctl-list.txt
@@ -0,0 +1,86 @@
+ndctl-list(1)
+=============
+
+NAME
+----
+ndctl-list - dump the platform nvdimm device topology in json
+
+SYNOPSIS
+--------
+[verse]
+'ndctl list' [<options>]
+
+Walk all the nvdimm buses in the system and list all attached devices
+along with some of their major attributes.
+
+Options can be specified to limit the output to devices of a certain
+class.  Where the classes are buses, dimms, regions, and namespaces.  By
+default, 'ndctl list' with no options is equivalent to:
+[verse]
+ndctl list --namespaces --bus=all --region=all
+
+EXAMPLE
+-------
+[verse]
+# ndctl list --buses --namespaces
+
+["literal"]
+{
+  "provider":"nfit_test.1",
+  "dev":"ndbus2",
+  "namespaces":[
+    {
+      "dev":"namespace9.0",
+      "mode":"raw",
+      "size":33554432,
+      "blockdev":"pmem9"
+    }
+  ]
+}
+{
+  "provider":"nfit_test.0",
+  "dev":"ndbus1"
+}
+{
+  "provider":"e820",
+  "dev":"ndbus0",
+  "namespaces":[
+    {
+      "dev":"namespace0.0",
+      "mode":"memory",
+      "size":8589934592,
+      "blockdev":"pmem0"
+    }
+  ]
+}
+
+OPTIONS
+-------
+-r::
+--region=::
+include::xable-region-options.txt[]
+
+-B::
+--buses::
+	Include bus info in the listing
+
+-D::
+--dimms::
+	Include dimm info in the listing
+
+-R::
+--regions::
+	Include region info in the listing
+
+-N::
+--namespaces::
+	Include namespace info in the listing.  Namespace info is
+	included by default, specify '--no-namespaces' to omit.
+
+-i::
+--idle::
+	Include idle (not enabled) devices in the listing
+
+SEE ALSO
+--------
+linkndctl:ndctl-create-namespace[1]
diff --git a/Makefile.am b/Makefile.am
index 28437488ab93..63e7301e99b8 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -15,7 +15,8 @@ AM_CPPFLAGS = \
 	-I${top_srcdir}/ \
 	$(KMOD_CFLAGS) \
 	$(UDEV_CFLAGS) \
-	$(UUID_CFLAGS)
+	$(UUID_CFLAGS) \
+	$(JSON_CFLAGS)
 
 AM_CFLAGS = ${my_CFLAGS} \
 	-fvisibility=hidden \
@@ -66,12 +67,14 @@ ndctl_SOURCES = ndctl.c \
 		builtin-create-nfit.c \
 		builtin-xaction-namespace.c \
 		builtin-xable-region.c \
+		builtin-list.c \
 		builtin-test.c \
 		builtin-help.c \
 		builtin-zero-labels.c \
 		util/parse-options.c \
 		util/parse-options.h \
 		util/usage.c \
+		util/json.c \
 		util/size.c \
 		util/strbuf.c \
 		util/wrapper.c \
@@ -91,7 +94,7 @@ ndctl_SOURCES += test/blk_namespaces.c \
 ndctl_SOURCES += builtin-bat.c
 endif
 
-ndctl_LDADD = lib/libndctl.la $(UUID_LIBS) $(KMOD_LIBS)
+ndctl_LDADD = lib/libndctl.la $(UUID_LIBS) $(KMOD_LIBS) $(JSON_LIBS)
 
 noinst_LIBRARIES = libccan.a
 libccan_a_SOURCES = \
diff --git a/builtin-list.c b/builtin-list.c
new file mode 100644
index 000000000000..12a6d32b124a
--- /dev/null
+++ b/builtin-list.c
@@ -0,0 +1,367 @@
+#include <stdio.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <limits.h>
+#include <util/json.h>
+#include <util/filter.h>
+#include <json-c/json.h>
+#include <ndctl/libndctl.h>
+#include <util/parse-options.h>
+#include <ccan/array_size/array_size.h>
+
+#ifdef HAVE_NDCTL_H
+#include <linux/ndctl.h>
+#else
+#include <ndctl.h>
+#endif
+
+static struct {
+	bool buses;
+	bool dimms;
+	bool regions;
+	bool namespaces;
+	bool idle;
+} list = {
+	.namespaces = true,
+};
+
+static struct {
+	const char *bus;
+	const char *region;
+} param;
+
+static int did_fail;
+static int jflag = JSON_C_TO_STRING_PRETTY;
+
+#define fail(fmt, ...) \
+do { \
+	did_fail = 1; \
+	fprintf(stderr, "ndctl-%s:%s:%d: " fmt, \
+			VERSION, __func__, __LINE__, ##__VA_ARGS__); \
+} while (0)
+
+static struct json_object *list_namespaces(struct ndctl_region *region,
+		struct json_object *container, struct json_object *jnamespaces,
+		bool continue_array)
+{
+	struct ndctl_namespace *ndns;
+
+	ndctl_namespace_foreach(region, ndns) {
+		struct json_object *jndns;
+
+		/* are we emitting namespaces? */
+		if (!list.namespaces)
+			break;
+
+		if (!list.idle && !util_namespace_active(ndns))
+			continue;
+
+		if (!jnamespaces) {
+			jnamespaces = json_object_new_array();
+			if (!jnamespaces) {
+				fail("\n");
+				continue;
+			}
+
+			if (container)
+				json_object_object_add(container, "namespaces",
+						jnamespaces);
+		}
+
+		jndns = util_namespace_to_json(ndns);
+		if (!jndns) {
+			fail("\n");
+			continue;
+		}
+
+		json_object_array_add(jnamespaces, jndns);
+	}
+
+	/*
+	 * We we are collecting namespaces anonymously across the
+	 * platform / bus
+	 */
+	if (continue_array)
+		return jnamespaces;
+	return NULL;
+}
+
+static struct json_object *region_to_json(struct ndctl_region *region)
+{
+	struct json_object *jregion = json_object_new_object();
+	struct json_object *jobj, *jmappings = NULL;
+	struct ndctl_interleave_set *iset;
+	struct ndctl_mapping *mapping;
+
+	if (!jregion)
+		return NULL;
+
+	jobj = json_object_new_string(ndctl_region_get_devname(region));
+	if (!jobj)
+		goto err;
+	json_object_object_add(jregion, "dev", jobj);
+
+	jobj = json_object_new_int64(ndctl_region_get_size(region));
+	if (!jobj)
+		goto err;
+	json_object_object_add(jregion, "size", jobj);
+
+	jobj = json_object_new_int64(ndctl_region_get_available_size(region));
+	if (!jobj)
+		goto err;
+	json_object_object_add(jregion, "available_size", jobj);
+
+	switch (ndctl_region_get_type(region)) {
+	case ND_DEVICE_REGION_PMEM:
+		jobj = json_object_new_string("pmem");
+		break;
+	case ND_DEVICE_REGION_BLK:
+		jobj = json_object_new_string("blk");
+		break;
+	default:
+		jobj = NULL;
+	}
+	if (!jobj)
+		goto err;
+	json_object_object_add(jregion, "type", jobj);
+
+	iset = ndctl_region_get_interleave_set(region);
+	if (iset) {
+		jobj = json_object_new_int64(
+				ndctl_interleave_set_get_cookie(iset));
+		if (!jobj)
+			fail("\n");
+		else
+			json_object_object_add(jregion, "iset_id", jobj);
+	}
+
+	ndctl_mapping_foreach(region, mapping) {
+		struct ndctl_dimm *dimm = ndctl_mapping_get_dimm(mapping);
+		struct json_object *jmapping;
+
+		if (!list.dimms)
+			break;
+
+		if (!list.idle && !ndctl_dimm_is_enabled(dimm))
+			continue;
+
+		if (!jmappings) {
+			jmappings = json_object_new_array();
+			if (!jmappings) {
+				fail("\n");
+				continue;
+			}
+			json_object_object_add(jregion, "mappings", jmappings);
+		}
+
+		jmapping = util_mapping_to_json(mapping);
+		if (!jmapping) {
+			fail("\n");
+			continue;
+		}
+		json_object_array_add(jmappings, jmapping);
+	}
+
+	if (!ndctl_region_is_enabled(region)) {
+		jobj = json_object_new_string("disabled");
+		if (!jobj)
+			goto err;
+		json_object_object_add(jregion, "state", jobj);
+	}
+
+	list_namespaces(region, jregion, NULL, false);
+	return jregion;
+ err:
+	fail("\n");
+	json_object_put(jregion);
+	return NULL;
+}
+
+static void display_array(struct json_object *jarray)
+{
+	if (json_object_array_length(jarray) > 1)
+		printf("%s\n", json_object_to_json_string_ext(jarray, jflag));
+	else {
+		struct json_object *jobj;
+
+		jobj = json_object_array_get_idx(jarray, 0);
+		printf("%s\n", json_object_to_json_string_ext(jobj, jflag));
+	}
+	json_object_put(jarray);
+}
+
+int cmd_list(int argc, const char **argv)
+{
+	const struct option options[] = {
+		OPT_STRING('b', "bus", &param.bus, "bus-id", "filter by bus"),
+		OPT_STRING('r', "region", &param.region, "region-id",
+				"filter by region"),
+		OPT_BOOLEAN('B', "buses", &list.buses, "include bus info"),
+		OPT_BOOLEAN('D', "dimms", &list.dimms, "include dimm info"),
+		OPT_BOOLEAN('R', "regions", &list.regions,
+				"include region info"),
+		OPT_BOOLEAN('N', "namespaces", &list.namespaces,
+				"include namespace info (default)"),
+		OPT_BOOLEAN('i', "idle", &list.idle, "include idle devices"),
+		OPT_END(),
+	};
+	const char * const u[] = {
+		"ndctl list [<options>]",
+		NULL
+	};
+	struct json_object *jnamespaces = NULL;
+	struct json_object *jregions = NULL;
+	struct json_object *jdimms = NULL;
+	struct json_object *jbuses = NULL;
+	struct ndctl_ctx *ctx;
+	struct ndctl_bus *bus;
+	int i, rc;
+
+        argc = parse_options(argc, argv, options, u, 0);
+	for (i = 0; i < argc; i++)
+		error("unknown parameter \"%s\"\n", argv[i]);
+	if (argc)
+		usage_with_options(u, options);
+
+	rc = ndctl_new(&ctx);
+	if (rc < 0)
+		return rc;
+
+	ndctl_bus_foreach(ctx, bus) {
+		struct json_object *jbus = NULL;
+		struct ndctl_region *region;
+		struct ndctl_dimm *dimm;
+
+		if (!util_bus_filter(bus, param.bus))
+			continue;
+
+		if (list.buses) {
+			if (!jbuses) {
+				jbuses = json_object_new_array();
+				if (!jbuses) {
+					fail("\n");
+					continue;
+				}
+			}
+
+			jbus = util_bus_to_json(bus);
+			if (!jbus) {
+				fail("\n");
+				continue;
+			}
+			json_object_array_add(jbuses, jbus);
+		}
+
+		ndctl_dimm_foreach(bus, dimm) {
+			struct json_object *jdimm;
+
+			/* are we emitting dimms? */
+			if (!list.dimms)
+				break;
+
+			if (!list.idle && !ndctl_dimm_is_enabled(dimm))
+				continue;
+
+			if (!jdimms) {
+				jdimms = json_object_new_array();
+				if (!jdimms) {
+					fail("\n");
+					continue;
+				}
+
+				if (jbus)
+					json_object_object_add(jbus, "dimms", jdimms);
+			}
+
+			jdimm = util_dimm_to_json(dimm);
+			if (!jdimm) {
+				fail("\n");
+				continue;
+			}
+
+			/*
+			 * Without a bus we are collecting dimms anonymously
+			 * across the platform.
+			 */
+			json_object_array_add(jdimms, jdimm);
+		}
+
+		ndctl_region_foreach(bus, region) {
+			struct json_object *jregion;
+
+			if (!util_region_filter(region, param.region))
+				continue;
+
+			if (!list.regions) {
+				jnamespaces = list_namespaces(region, jbus,
+						jnamespaces, true);
+				continue;
+			}
+
+			if (!list.idle && !ndctl_region_is_enabled(region))
+				continue;
+
+			if (!jregions) {
+				jregions = json_object_new_array();
+				if (!jregions) {
+					fail("\n");
+					continue;
+				}
+
+				if (jbus)
+					json_object_object_add(jbus, "regions",
+							jregions);
+			}
+
+			jregion = region_to_json(region);
+			if (!jregion) {
+				fail("\n");
+				continue;
+			}
+
+			/*
+			 * Without a bus we are collecting regions anonymously
+			 * across the platform.
+			 */
+			json_object_array_add(jregions, jregion);
+		}
+
+		if (jbuses) {
+			jdimms = NULL;
+			jregions = NULL;
+			jnamespaces = NULL;
+		}
+	}
+
+	if (jbuses)
+		display_array(jbuses);
+	else if ((!!jdimms + !!jregions + !!jnamespaces) > 1) {
+		struct json_object *jplatform = json_object_new_object();
+
+		if (!jplatform) {
+			fail("\n");
+			return -ENOMEM;
+		}
+
+		if (jdimms)
+			json_object_object_add(jplatform, "dimms", jdimms);
+		if (jregions)
+			json_object_object_add(jplatform, "regions", jregions);
+		if (jnamespaces)
+			json_object_object_add(jplatform, "namespaces",
+					jnamespaces);
+		printf("%s\n", json_object_to_json_string_ext(jplatform,
+					jflag));
+		json_object_put(jplatform);
+	} else if (jdimms)
+		display_array(jdimms);
+	else if (jregions)
+		display_array(jregions);
+	else if (jnamespaces)
+		display_array(jnamespaces);
+
+	if (did_fail)
+		return -ENOMEM;
+	return 0;
+}
diff --git a/builtin.h b/builtin.h
index 27ff248a6d84..bec95556a524 100644
--- a/builtin.h
+++ b/builtin.h
@@ -16,6 +16,7 @@ int cmd_enable_region(int argc, const char **argv);
 int cmd_disable_region(int argc, const char **argv);
 int cmd_zero_labels(int argc, const char **argv);
 int cmd_help(int argc, const char **argv);
+int cmd_list(int argc, const char **argv);
 #ifdef ENABLE_TEST
 int cmd_test(int argc, const char **argv);
 #endif
diff --git a/configure.ac b/configure.ac
index 5f25b56aca9a..3f61b6de250e 100644
--- a/configure.ac
+++ b/configure.ac
@@ -82,6 +82,7 @@ AM_CONDITIONAL([ENABLE_DESTRUCTIVE], [test "x$enable_destructive" = "xyes"])
 PKG_CHECK_MODULES([KMOD], [libkmod])
 PKG_CHECK_MODULES([UDEV], [libudev])
 PKG_CHECK_MODULES([UUID], [uuid])
+PKG_CHECK_MODULES([JSON], [json-c])
 
 AC_ARG_ENABLE([local],
         AS_HELP_STRING([--enable-local], [build with local ndctl.h @<:@default=system@:>@]),
diff --git a/contrib/ndctl.spec.in b/contrib/ndctl.spec.in
index bb224a13b151..9a1acd48760d 100644
--- a/contrib/ndctl.spec.in
+++ b/contrib/ndctl.spec.in
@@ -21,6 +21,7 @@ BuildRequires:  pkgconfig
 BuildRequires:  pkgconfig(libkmod)
 BuildRequires:  pkgconfig(libudev)
 BuildRequires:  pkgconfig(uuid)
+BuildRequires:  pkgconfig(json-c)
 
 %description
 Utility library for managing the "libnvdimm" subsystem.  The "libnvdimm"
diff --git a/ndctl.c b/ndctl.c
index 0c117d7fbd2f..8fadc6b2e305 100644
--- a/ndctl.c
+++ b/ndctl.c
@@ -30,6 +30,7 @@ static struct cmd_struct commands[] = {
 	{ "enable-region", cmd_enable_region },
 	{ "disable-region", cmd_disable_region },
 	{ "zero-labels", cmd_zero_labels },
+	{ "list", cmd_list },
 	{ "help", cmd_help },
 	#ifdef ENABLE_TEST
 	{ "test", cmd_test },
diff --git a/util/json.c b/util/json.c
new file mode 100644
index 000000000000..b9a5e75164d8
--- /dev/null
+++ b/util/json.c
@@ -0,0 +1,208 @@
+#include <util/json.h>
+#include <uuid/uuid.h>
+#include <json-c/json.h>
+#include <ndctl/libndctl.h>
+#include <ccan/array_size/array_size.h>
+
+#ifdef HAVE_NDCTL_H
+#include <linux/ndctl.h>
+#else
+#include <ndctl.h>
+#endif
+
+struct json_object *util_bus_to_json(struct ndctl_bus *bus)
+{
+	struct json_object *jbus = json_object_new_object();
+	struct json_object *jobj;
+
+	if (!jbus)
+		return NULL;
+
+	jobj = json_object_new_string(ndctl_bus_get_provider(bus));
+	if (!jobj)
+		goto err;
+	json_object_object_add(jbus, "provider", jobj);
+
+	jobj = json_object_new_string(ndctl_bus_get_devname(bus));
+	if (!jobj)
+		goto err;
+	json_object_object_add(jbus, "dev", jobj);
+
+	return jbus;
+ err:
+	json_object_put(jbus);
+	return NULL;
+}
+
+struct json_object *util_dimm_to_json(struct ndctl_dimm *dimm)
+{
+	struct json_object *jdimm = json_object_new_object();
+	struct json_object *jobj;
+
+	if (!jdimm)
+		return NULL;
+
+	jobj = json_object_new_string(ndctl_dimm_get_devname(dimm));
+	if (!jobj)
+		goto err;
+	json_object_object_add(jdimm, "dev", jobj);
+
+	if (!ndctl_dimm_is_enabled(dimm)) {
+		jobj = json_object_new_string("disabled");
+		if (!jobj)
+			goto err;
+		json_object_object_add(jdimm, "state", jobj);
+	}
+
+	return jdimm;
+ err:
+	json_object_put(jdimm);
+	return NULL;
+}
+
+bool util_namespace_active(struct ndctl_namespace *ndns)
+{
+	struct ndctl_btt *btt = ndctl_namespace_get_btt(ndns);
+	struct ndctl_pfn *pfn = ndctl_namespace_get_pfn(ndns);
+
+	if ((btt && ndctl_btt_is_enabled(btt))
+			|| (pfn && ndctl_pfn_is_enabled(pfn))
+			|| (!btt && !pfn
+				&& ndctl_namespace_is_enabled(ndns)))
+		return true;
+	return false;
+}
+
+struct json_object *util_namespace_to_json(struct ndctl_namespace *ndns)
+{
+	struct json_object *jndns = json_object_new_object();
+	struct json_object *jobj;
+	const char *bdev = NULL;
+	struct ndctl_btt *btt;
+	struct ndctl_pfn *pfn;
+	char buf[40];
+	uuid_t uuid;
+
+	if (!jndns)
+		return NULL;
+
+	jobj = json_object_new_string(ndctl_namespace_get_devname(ndns));
+	if (!jobj)
+		goto err;
+	json_object_object_add(jndns, "dev", jobj);
+
+	switch (ndctl_namespace_get_mode(ndns)) {
+	case NDCTL_NS_MODE_MEMORY:
+		jobj = json_object_new_string("memory");
+		break;
+	case NDCTL_NS_MODE_SAFE:
+		jobj = json_object_new_string("sector");
+		break;
+	case NDCTL_NS_MODE_RAW:
+		jobj = json_object_new_string("raw");
+		break;
+	default:
+		jobj = NULL;
+	}
+	if (jobj)
+		json_object_object_add(jndns, "mode", jobj);
+
+	jobj = json_object_new_int64(ndctl_namespace_get_size(ndns));
+	if (!jobj)
+		goto err;
+	json_object_object_add(jndns, "size", jobj);
+
+	btt = ndctl_namespace_get_btt(ndns);
+	pfn = ndctl_namespace_get_pfn(ndns);
+	if (btt) {
+		ndctl_btt_get_uuid(btt, uuid);
+		uuid_unparse(uuid, buf);
+		jobj = json_object_new_string(buf);
+		if (!jobj)
+			goto err;
+		json_object_object_add(jndns, "uuid", jobj);
+
+		jobj = json_object_new_int(ndctl_btt_get_sector_size(btt));
+		if (!jobj)
+			goto err;
+		json_object_object_add(jndns, "sector_size", jobj);
+
+		bdev = ndctl_btt_get_block_device(btt);
+	} else if (pfn) {
+		ndctl_pfn_get_uuid(pfn, uuid);
+		uuid_unparse(uuid, buf);
+		jobj = json_object_new_string(buf);
+		if (!jobj)
+			goto err;
+		json_object_object_add(jndns, "uuid", jobj);
+		bdev = ndctl_pfn_get_block_device(pfn);
+	} else if (ndctl_namespace_get_type(ndns) != ND_DEVICE_NAMESPACE_IO) {
+		const char *name;
+
+		ndctl_namespace_get_uuid(ndns, uuid);
+		uuid_unparse(uuid, buf);
+		jobj = json_object_new_string(buf);
+		if (!jobj)
+			goto err;
+		json_object_object_add(jndns, "uuid", jobj);
+
+		name = ndctl_namespace_get_alt_name(ndns);
+		if (name[0]) {
+			jobj = json_object_new_string(name);
+			if (!jobj)
+				goto err;
+			json_object_object_add(jndns, "name", jobj);
+		}
+		bdev = ndctl_namespace_get_block_device(ndns);
+	} else
+		bdev = ndctl_namespace_get_block_device(ndns);
+
+	if (bdev && bdev[0]) {
+		jobj = json_object_new_string(bdev);
+		if (!jobj)
+			goto err;
+		json_object_object_add(jndns, "blockdev", jobj);
+	}
+
+	if (!util_namespace_active(ndns)) {
+		jobj = json_object_new_string("disabled");
+		if (!jobj)
+			goto err;
+		json_object_object_add(jndns, "state", jobj);
+	}
+
+	return jndns;
+ err:
+	json_object_put(jndns);
+	return NULL;
+}
+
+struct json_object *util_mapping_to_json(struct ndctl_mapping *mapping)
+{
+	struct json_object *jmapping = json_object_new_object();
+	struct ndctl_dimm *dimm = ndctl_mapping_get_dimm(mapping);
+	struct json_object *jobj;
+
+	if (!jmapping)
+		return NULL;
+
+	jobj = json_object_new_string(ndctl_dimm_get_devname(dimm));
+	if (!jobj)
+		goto err;
+	json_object_object_add(jmapping, "dimm", jobj);
+
+	jobj = json_object_new_int64(ndctl_mapping_get_offset(mapping));
+	if (!jobj)
+		goto err;
+	json_object_object_add(jmapping, "offset", jobj);
+
+	jobj = json_object_new_int64(ndctl_mapping_get_length(mapping));
+	if (!jobj)
+		goto err;
+	json_object_object_add(jmapping, "length", jobj);
+
+	return jmapping;
+ err:
+	json_object_put(jmapping);
+	return NULL;
+}
diff --git a/util/json.h b/util/json.h
new file mode 100644
index 000000000000..653bbd9beef1
--- /dev/null
+++ b/util/json.h
@@ -0,0 +1,11 @@
+#ifndef __NDCTL_JSON_H__
+#define __NDCTL_JSON_H__
+#include <stdbool.h>
+#include <ndctl/libndctl.h>
+
+bool util_namespace_active(struct ndctl_namespace *ndns);
+struct json_object *util_bus_to_json(struct ndctl_bus *bus);
+struct json_object *util_dimm_to_json(struct ndctl_dimm *dimm);
+struct json_object *util_mapping_to_json(struct ndctl_mapping *mapping);
+struct json_object *util_namespace_to_json(struct ndctl_namespace *ndns);
+#endif /* __NDCTL_JSON_H__ */
