diff --git a/lib/command-line.c b/lib/command-line.c
index 967f4f5d5..2da9f5fb1 100644
--- a/lib/command-line.c
+++ b/lib/command-line.c
@@ -53,6 +53,46 @@ ovs_cmdl_long_options_to_short_options(const struct option options[])
     return xstrdup(short_options);
 }
 
+static char * OVS_WARN_UNUSED_RESULT
+ovs_cmdl_parse_output_fmt__(const char * arg, enum ovs_output_fmt *fmt)
+{
+    if (!strcmp(arg, "text")) {
+        *fmt = OVS_OUTPUT_FMT_TEXT;
+    } else if (!strcmp(arg, "json")) {
+        *fmt = OVS_OUTPUT_FMT_JSON;
+    } else {
+        return xasprintf("unsupported output format '%s'", arg);
+    }
+    return NULL;
+}
+
+char * OVS_WARN_UNUSED_RESULT
+ovs_cmdl_parse_output_fmt(int argi, int argc, const char *argv[],
+                          enum ovs_output_fmt *fmt, int *parsed)
+{
+    ovs_assert(argi < argc);
+    const char *arg = argv[argi];
+    *parsed = 0;
+
+    if (!strncmp(arg, "--output=", 9)) {
+        *parsed = 2;
+        if (strlen(arg) <= 9) {
+            return xasprintf("option '%s' requires an argument", arg);
+        }
+        return ovs_cmdl_parse_output_fmt__(arg+9, fmt);
+    } else if (!strcmp(arg, "-o") || !strcmp(arg, "--output")) {
+        *parsed = 2;
+        if (argi+1 >= argc) {
+            return xasprintf("option '%s' requires an argument", arg);
+        }
+        return ovs_cmdl_parse_output_fmt__(argv[argi+1], fmt);
+    } else {
+        return NULL;
+    }
+
+    OVS_NOT_REACHED();
+}
+
 static char * OVS_WARN_UNUSED_RESULT
 build_short_options(const struct option *long_options)
 {
diff --git a/lib/command-line.h b/lib/command-line.h
index fc2a2690f..d04377098 100644
--- a/lib/command-line.h
+++ b/lib/command-line.h
@@ -46,6 +46,14 @@ struct ovs_cmdl_command {
 
 char *ovs_cmdl_long_options_to_short_options(const struct option *options);
 
+enum ovs_output_fmt {
+    OVS_OUTPUT_FMT_TEXT,
+    OVS_OUTPUT_FMT_JSON
+};
+
+char * ovs_cmdl_parse_output_fmt(int argi, int argc, const char *argv[],
+                                 enum ovs_output_fmt *fmt, int *parsed);
+
 struct ovs_cmdl_parsed_option {
     const struct option *o;
     char *arg;
diff --git a/lib/unixctl.c b/lib/unixctl.c
index 103357ee9..b83429e3d 100644
--- a/lib/unixctl.c
+++ b/lib/unixctl.c
@@ -21,7 +21,6 @@
 #include "coverage.h"
 #include "dirs.h"
 #include "openvswitch/dynamic-string.h"
-#include "openvswitch/json.h"
 #include "jsonrpc.h"
 #include "openvswitch/list.h"
 #include "openvswitch/poll-loop.h"
@@ -128,36 +127,41 @@ unixctl_command_register(const char *name, const char *usage,
     shash_add(&commands, name, command);
 }
 
-static void
-unixctl_command_reply__(struct unixctl_conn *conn,
-                        bool success, const char *body)
+static struct json *
+json_string_create__(const char *body)
 {
-    struct json *body_json;
-    struct jsonrpc_msg *reply;
-
-    COVERAGE_INC(unixctl_replied);
-    ovs_assert(conn->request_id);
-
     if (!body) {
         body = "";
     }
 
     if (body[0] && body[strlen(body) - 1] != '\n') {
-        body_json = json_string_create_nocopy(xasprintf("%s\n", body));
+        return json_string_create_nocopy(xasprintf("%s\n", body));
     } else {
-        body_json = json_string_create(body);
+        return json_string_create(body);
     }
+}
+
+static void
+unixctl_command_reply__(struct unixctl_conn *conn,
+                        bool success, struct json *body)
+{
+    struct jsonrpc_msg *reply;
+
+    COVERAGE_INC(unixctl_replied);
+    ovs_assert(conn->request_id);
 
     if (success) {
-        reply = jsonrpc_create_reply(body_json, conn->request_id);
+        reply = jsonrpc_create_reply(body, conn->request_id);
     } else {
-        reply = jsonrpc_create_error(body_json, conn->request_id);
+        reply = jsonrpc_create_error(body, conn->request_id);
     }
 
     if (VLOG_IS_DBG_ENABLED()) {
         char *id = json_to_string(conn->request_id, 0);
+        char *msg = json_to_string(body, 0);
         VLOG_DBG("replying with %s, id=%s: \"%s\"",
-                 success ? "success" : "error", id, body);
+                 success ? "success" : "error", id, msg);
+        free(msg);
         free(id);
     }
 
@@ -170,22 +174,36 @@ unixctl_command_reply__(struct unixctl_conn *conn,
 
 /* Replies to the active unixctl connection 'conn'.  'result' is sent to the
  * client indicating the command was processed successfully.  Only one call to
- * unixctl_command_reply() or unixctl_command_reply_error() may be made per
- * request. */
+ * unixctl_command_reply(), unixctl_command_reply_error() or
+ * unixctl_command_reply_json() may be made per request. */
 void
 unixctl_command_reply(struct unixctl_conn *conn, const char *result)
 {
-    unixctl_command_reply__(conn, true, result);
+    struct json *body = json_string_create__(result);
+    unixctl_command_reply__(conn, true, body);
+    json_destroy(body);
 }
 
 /* Replies to the active unixctl connection 'conn'. 'error' is sent to the
- * client indicating an error occurred processing the command.  Only one call to
- * unixctl_command_reply() or unixctl_command_reply_error() may be made per
- * request. */
+ * client indicating an error occurred processing the command.  Only one call
+ * to unixctl_command_reply(), unixctl_command_reply_error() or
+ * unixctl_command_reply_json() may be made per request. */
 void
 unixctl_command_reply_error(struct unixctl_conn *conn, const char *error)
 {
-    unixctl_command_reply__(conn, false, error);
+    struct json *body = json_string_create__(error);
+    unixctl_command_reply__(conn, false, body);
+    json_destroy(body);
+}
+
+/* Replies to the active unixctl connection 'conn'.  'result' is sent to the
+ * client indicating the command was processed successfully.  Only one call to
+ * unixctl_command_reply(), unixctl_command_reply_error() or
+ * unixctl_command_reply_json() may be made per request. */
+void
+unixctl_command_reply_json(struct unixctl_conn *conn, struct json *body)
+{
+    unixctl_command_reply__(conn, true, body);
 }
 
 /* Creates a unixctl server listening on 'path', which for POSIX may be:
@@ -518,6 +536,20 @@ unixctl_client_transact(struct jsonrpc *client, const char *command, int argc,
     } else if (reply->result) {
         if (reply->result->type == JSON_STRING) {
             *result = xstrdup(json_string(reply->result));
+        } else if (reply->result->type == JSON_OBJECT ||
+                   reply->result->type == JSON_ARRAY) {
+            /* TODO: How about other result types? */
+
+            /* TODO: Do we really want to prettyfy and sort the output?
+             * The benefit for users is probably minimal because they could
+             * simply use jq to format the output if needed. Since JSON output
+             * is meant to be consumed by machines, this pretty-printing is
+             * probably unnecessary in most cases.
+             * However, it might have its use in our unit tests because it
+             * allows us to make readable checks without having to introduce a
+             * dependency on jq.
+             */
+            *result = json_to_string(reply->result, JSSF_PRETTY | JSSF_SORT);
         } else {
             VLOG_WARN("%s: unexpected result type in JSON rpc reply: %s",
                       jsonrpc_get_name(client),
diff --git a/lib/unixctl.h b/lib/unixctl.h
index 4562dbc49..87c45fa14 100644
--- a/lib/unixctl.h
+++ b/lib/unixctl.h
@@ -17,6 +17,8 @@
 #ifndef UNIXCTL_H
 #define UNIXCTL_H 1
 
+#include "openvswitch/json.h"
+
 #ifdef  __cplusplus
 extern "C" {
 #endif
@@ -47,6 +49,7 @@ void unixctl_command_register(const char *name, const char *usage,
                               unixctl_cb_func *cb, void *aux);
 void unixctl_command_reply_error(struct unixctl_conn *, const char *error);
 void unixctl_command_reply(struct unixctl_conn *, const char *body);
+void unixctl_command_reply_json(struct unixctl_conn *, struct json *body);
 
 #ifdef  __cplusplus
 }
diff --git a/ofproto/ofproto-dpif.c b/ofproto/ofproto-dpif.c
index ba5706f6a..27d617a8f 100644
--- a/ofproto/ofproto-dpif.c
+++ b/ofproto/ofproto-dpif.c
@@ -20,6 +20,7 @@
 #include "bond.h"
 #include "bundle.h"
 #include "byte-order.h"
+#include "command-line.h"
 #include "connectivity.h"
 #include "connmgr.h"
 #include "coverage.h"
@@ -6344,8 +6345,101 @@ done:
     return changed;
 }
 
+static struct json *
+dpif_show_backer_json(const struct dpif_backer *backer)
+{
+    struct json *json_backer = json_object_create();
+
+    /* name */
+    json_object_put_string(json_backer, "name", dpif_name(backer->dpif));
+
+    /* stats */
+    struct dpif_dp_stats dp_stats;
+    dpif_get_dp_stats(backer->dpif, &dp_stats);
+    struct json *json_dp_stats = json_object_create();
+    json_object_put_format(json_dp_stats, "n_hit", "%"PRIu64, dp_stats.n_hit);
+    json_object_put_format(json_dp_stats, "n_missed", "%"PRIu64,
+                           dp_stats.n_missed);
+    json_object_put(json_backer, "stats", json_dp_stats);
+
+    /* ofprotos */
+    struct json *json_ofprotos = json_array_create_empty();
+    struct shash ofproto_shash;
+    shash_init(&ofproto_shash);
+    const struct shash_node **ofprotos = get_ofprotos(&ofproto_shash);
+    for (size_t i = 0; i < shash_count(&ofproto_shash); i++) {
+        struct ofproto_dpif *ofproto = ofprotos[i]->data;
+
+        if (ofproto->backer != backer) {
+            continue;
+        }
+
+        struct json *json_ofproto = json_object_create();
+
+        /* ofproto name */
+        json_object_put_string(json_ofproto, "name", ofproto->up.name);
+
+        /* ofproto ports */
+        struct json *json_ofproto_ports = json_array_create_empty();
+        const struct shash_node **ports = shash_sort(&ofproto->up.port_by_name);
+        for (size_t j = 0; j < shash_count(&ofproto->up.port_by_name); j++) {
+            const struct shash_node *port = ports[j];
+            struct ofport *ofport = port->data;
+
+            struct json * json_ofproto_port = json_object_create();
+            /* ofproto port netdev name */
+            json_object_put_string(json_ofproto_port, "netdev_name",
+                                   netdev_get_name(ofport->netdev));
+            /* ofproto port ofp port */
+            json_object_put_format(json_ofproto_port, "ofp_port", "%u",
+                                   ofport->ofp_port);
+
+            /* ofproto port odp port */
+            odp_port_t odp_port = ofp_port_to_odp_port(ofproto,
+                                                       ofport->ofp_port);
+            if (odp_port != ODPP_NONE) {
+                json_object_put_format(json_ofproto_port, "odp_port",
+                                       "%"PRIu32, odp_port);
+            } else {
+                json_object_put_string(json_ofproto_port, "odp_port", "none");
+            }
+
+            /* ofproto port netdev type */
+            json_object_put_string(json_ofproto_port, "netdev_type",
+                                   netdev_get_type(ofport->netdev));
+
+            /* ofproto port config */
+            struct json *json_port_config = json_object_create();
+            struct smap config;
+            smap_init(&config);
+            if (!netdev_get_config(ofport->netdev, &config)) {
+                struct smap_node *node;
+                SMAP_FOR_EACH(node, &config) {
+                    json_object_put_string(json_port_config, node->key,
+                                           node->value);
+                }
+            }
+            smap_destroy(&config);
+            json_object_put(json_ofproto_port, "netdev_config",
+                            json_port_config);
+
+            json_array_add(json_ofproto_ports, json_ofproto_port);
+        } /* ofproto port */
+
+        free(ports);
+        json_object_put(json_ofproto, "ports", json_ofproto_ports);
+
+        json_array_add(json_ofprotos, json_ofproto);
+    } /* ofproto */
+    shash_destroy(&ofproto_shash);
+    free(ofprotos);
+
+    json_object_put(json_backer, "ofprotos", json_ofprotos);
+    return json_backer;
+}
+
 static void
-dpif_show_backer(const struct dpif_backer *backer, struct ds *ds)
+dpif_show_backer_text(const struct dpif_backer *backer, struct ds *ds)
 {
     const struct shash_node **ofprotos;
     struct dpif_dp_stats dp_stats;
@@ -6409,21 +6503,53 @@ dpif_show_backer(const struct dpif_backer *backer, struct ds *ds)
 }
 
 static void
-ofproto_unixctl_dpif_show(struct unixctl_conn *conn, int argc OVS_UNUSED,
-                          const char *argv[] OVS_UNUSED, void *aux OVS_UNUSED)
+ofproto_unixctl_dpif_show(struct unixctl_conn *conn, int argc,
+                          const char *argv[], void *aux OVS_UNUSED)
 {
     struct ds ds = DS_EMPTY_INITIALIZER;
-    const struct shash_node **backers;
-    int i;
+    enum ovs_output_fmt fmt = OVS_OUTPUT_FMT_TEXT;
 
-    backers = shash_sort(&all_dpif_backers);
-    for (i = 0; i < shash_count(&all_dpif_backers); i++) {
-        dpif_show_backer(backers[i]->data, &ds);
+    /* Parse options. Both getopt() and getopt_long() cannot be used here
+     * because they use global variables and are already used by the caller.
+     */
+    /* TODO: Modify getopt()/getopt_long() to use local storage or use
+     *       alternatives like presented in
+     *       https://nullprogram.com/blog/2014/10/12/ ? */
+    for (int i = 1, parsed = 0; i < argc; i += parsed) {
+        char *error = ovs_cmdl_parse_output_fmt(i, argc, argv, &fmt, &parsed);
+
+        if (error) {
+            unixctl_command_reply_error(conn, error);
+            free(error);
+            return;
+        }
+
+        if (!parsed) {
+            ds_put_format(&ds, "Unrecognized option %s", argv[i]);
+            unixctl_command_reply_error(conn, ds_cstr(&ds));
+            ds_destroy(&ds);
+            return;
+        }
     }
-    free(backers);
 
-    unixctl_command_reply(conn, ds_cstr(&ds));
-    ds_destroy(&ds);
+    if (fmt == OVS_OUTPUT_FMT_JSON) {
+        struct json *backers = json_array_create_empty();
+        const struct shash_node *backer;
+        SHASH_FOR_EACH(backer, &all_dpif_backers) {
+            json_array_add(backers, dpif_show_backer_json(backer->data));
+        }
+        unixctl_command_reply_json(conn, backers);
+        json_destroy(backers);
+    } else {
+        const struct shash_node **backers = shash_sort(&all_dpif_backers);
+        for (int i = 0; i < shash_count(&all_dpif_backers); i++) {
+            dpif_show_backer_text(backers[i]->data, &ds);
+        }
+        free(backers);
+
+        unixctl_command_reply(conn, ds_cstr(&ds));
+        ds_destroy(&ds);
+    }
 }
 
 static void
@@ -6595,8 +6721,8 @@ ofproto_unixctl_init(void)
                              ofproto_unixctl_mcast_snooping_show, NULL);
     unixctl_command_register("dpif/dump-dps", "", 0, 0,
                              ofproto_unixctl_dpif_dump_dps, NULL);
-    unixctl_command_register("dpif/show", "", 0, 0, ofproto_unixctl_dpif_show,
-                             NULL);
+    unixctl_command_register("dpif/show", "[(-o|--output=)text|json]", 0, 2,
+                             ofproto_unixctl_dpif_show, NULL);
     unixctl_command_register("dpif/show-dp-features", "bridge", 1, 1,
                              ofproto_unixctl_dpif_show_dp_features, NULL);
     unixctl_command_register("dpif/dump-flows",
diff --git a/tests/pmd.at b/tests/pmd.at
index 7bdaca9e7..154fd48a3 100644
--- a/tests/pmd.at
+++ b/tests/pmd.at
@@ -100,6 +100,35 @@ dummy@ovs-dummy: hit:0 missed:0
     p0 1/1: (dummy-pmd: configured_rx_queues=1, configured_tx_queues=<cleared>, requested_rx_queues=1, requested_tx_queues=<cleared>)
 ])
 
+AT_CHECK([ovs-appctl dpif/show --output json | sed 's/\(tx_queues": "\)[[0-9]]*"/\1<cleared>"/g'], [0], [dnl
+[[
+  {
+    "name": "dummy@ovs-dummy",
+    "ofprotos": [
+      {
+        "name": "br0",
+        "ports": [
+          {
+            "netdev_config": {
+              },
+            "netdev_name": "br0",
+            "netdev_type": "dummy-internal",
+            "odp_port": "100",
+            "ofp_port": "65534"},
+          {
+            "netdev_config": {
+              "configured_rx_queues": "1",
+              "configured_tx_queues": "<cleared>",
+              "requested_rx_queues": "1",
+              "requested_tx_queues": "<cleared>"},
+            "netdev_name": "p0",
+            "netdev_type": "dummy-pmd",
+            "odp_port": "1",
+            "ofp_port": "1"}]}],
+    "stats": {
+      "n_hit": "0",
+      "n_missed": "0"}}]]])
+
 OVS_VSWITCHD_STOP
 AT_CLEANUP
 
