diff --git a/backbonetable.c b/backbonetable.c
index 17fbd1d..11cec32 100644
--- a/backbonetable.c
+++ b/backbonetable.c
@@ -98,7 +98,7 @@ static int netlink_print_bla_backbone(struct state *state, char *orig_iface,
 				    orig_timeout, watch_interval,
 				    "Originator           VID   last seen (CRC   )\n",
 				    BATADV_CMD_GET_BLA_BACKBONE,
-				    bla_backbone_callback);
+				    bla_backbone_callback, 0);
 }
 
 static struct debug_table_data batctl_debug_table_backbonetable = {
diff --git a/claimtable.c b/claimtable.c
index b6bf9f5..0bf017d 100644
--- a/claimtable.c
+++ b/claimtable.c
@@ -103,7 +103,7 @@ static int netlink_print_bla_claim(struct state *state, char *orig_iface,
 				    orig_timeout, watch_interval,
 				    "Client               VID      Originator        [o] (CRC   )\n",
 				    BATADV_CMD_GET_BLA_CLAIM,
-				    bla_claim_callback);
+				    bla_claim_callback, 0);
 }
 
 static struct debug_table_data batctl_debug_table_claimtable = {
diff --git a/dat_cache.c b/dat_cache.c
index 8d47171..109e553 100644
--- a/dat_cache.c
+++ b/dat_cache.c
@@ -116,7 +116,7 @@ static int netlink_print_dat_cache(struct state *state, char *orig_iface,
 	ret = netlink_print_common(state, orig_iface, read_opts,
 				   orig_timeout, watch_interval, header,
 				   BATADV_CMD_GET_DAT_CACHE,
-				   dat_cache_callback);
+				   dat_cache_callback, 0);
 
 	free(header);
 	return ret;
diff --git a/gateways.c b/gateways.c
index 7625bd8..867c882 100644
--- a/gateways.c
+++ b/gateways.c
@@ -141,7 +141,7 @@ static int netlink_print_gateways(struct state *state, char *orig_iface,
 				    orig_timeout, watch_interval,
 				    header,
 				    BATADV_CMD_GET_GATEWAYS,
-				    gateways_callback);
+				    gateways_callback, 0);
 }
 
 static struct debug_table_data batctl_debug_table_gateways = {
diff --git a/main.h b/main.h
index 81b7a27..b1ff050 100644
--- a/main.h
+++ b/main.h
@@ -44,6 +44,8 @@ extern char module_ver_path[];
 
 #define ARRAY_SIZE(x) (sizeof(x) / sizeof(*(x)))
 
+#define BOOL_STRING(x) (x ? "true" : "false")
+
 #ifndef container_of
 #define container_of(ptr, type, member) __extension__ ({ \
 	const __typeof__(((type *)0)->member) *__pmember = (ptr); \
diff --git a/mcast_flags.c b/mcast_flags.c
index 721f549..cb6e89d 100644
--- a/mcast_flags.c
+++ b/mcast_flags.c
@@ -147,7 +147,7 @@ static int netlink_print_mcast_flags(struct state *state, char *orig_iface,
 	ret = netlink_print_common(state, orig_iface, read_opts,
 				   orig_timeout, watch_interval, header,
 				   BATADV_CMD_GET_MCAST_FLAGS,
-				   mcast_flags_callback);
+				   mcast_flags_callback, 0);
 
 	free(header);
 	return ret;
diff --git a/neighbors.c b/neighbors.c
index af76d0f..2a50e78 100644
--- a/neighbors.c
+++ b/neighbors.c
@@ -114,7 +114,7 @@ static int netlink_print_neighbors(struct state *state, char *orig_iface,
 				    orig_timeout, watch_interval,
 				    "IF             Neighbor              last-seen\n",
 				    BATADV_CMD_GET_NEIGHBORS,
-				    neighbors_callback);
+				    neighbors_callback, 0);
 }
 
 static struct debug_table_data batctl_debug_table_neighbors = {
diff --git a/netlink.c b/netlink.c
index 31c9b01..26ae5ef 100644
--- a/netlink.c
+++ b/netlink.c
@@ -13,6 +13,7 @@
 #include <stdio.h>
 #include <string.h>
 #include <unistd.h>
+#include <ctype.h>
 #include <errno.h>
 #include <net/ethernet.h>
 #include <net/if.h>
@@ -116,6 +117,50 @@ struct nla_policy batadv_netlink_policy[NUM_BATADV_ATTR] = {
 	[BATADV_ATTR_THROUGHPUT_OVERRIDE]	= { .type = NLA_U32 },
 };
 
+struct nla_policy_json batadv_netlink_policy_json[NUM_BATADV_ATTR] = {
+	[BATADV_ATTR_VERSION]			= { .name = "version",
+						    .cb = nljson_print_str },
+	[BATADV_ATTR_ALGO_NAME]			= { .name = "algo",
+						    .cb = nljson_print_str },
+	[BATADV_ATTR_MESH_IFINDEX]		= { .name = "mesh_if_idx",
+						    .cb = nljson_print_ifname_by_ifindex },
+	[BATADV_ATTR_MESH_IFNAME]		= { .name = "mesh_if",
+						    .cb = nljson_print_str },
+	[BATADV_ATTR_MESH_ADDRESS]		= { .name = "mesh_address",
+						    .cb = nljson_print_mac},
+	[BATADV_ATTR_ORIG_ADDRESS]		= { .name = "originator",
+						    .cb = nljson_print_mac },
+	[BATADV_ATTR_HARD_IFINDEX]		= { .name = "hard_if_idx",
+						    .cb = nljson_print_ifname_by_ifindex },
+	[BATADV_ATTR_HARD_IFNAME]		= { .name = "hard_if",
+						    .cb = nljson_print_str },
+	[BATADV_ATTR_HARD_ADDRESS]		= { .name = "hard_addr",
+						    .cb = nljson_print_mac },
+	[BATADV_ATTR_TT_ADDRESS]		= { .name = "client",
+						    .cb = nljson_print_mac },
+	[BATADV_ATTR_TT_TTVN]			= { .name = "ttvn",
+						    .cb = nljson_print_uint8_t },
+	[BATADV_ATTR_TT_LAST_TTVN]		= { .name = "last_ttvn",
+						    .cb = nljson_print_uint8_t },
+	[BATADV_ATTR_TT_CRC32]			= { .name = "crc32",
+						    .cb = nljson_print_crc32 },
+	[BATADV_ATTR_TT_VID]			= { .name = "vid",
+						    .cb = nljson_print_vid },
+	[BATADV_ATTR_TT_FLAGS]			= { .name = "flags",
+						    .cb = nljson_print_ttflags },
+	[BATADV_ATTR_FLAG_BEST]			= { .name = "best",
+						    .cb = nljson_print_bool},
+	[BATADV_ATTR_LAST_SEEN_MSECS]		= { .name = "last_seen",
+						    .cb = nljson_print_int},
+	[BATADV_ATTR_NEIGH_ADDRESS]		= { .name = "neighbor",
+						    .cb = nljson_print_mac },
+	[BATADV_ATTR_TQ]			= { .name = "tq",
+						    .cb = nljson_print_uint8_t },
+	[BATADV_ATTR_THROUGHPUT]		= { .name = "throughput",
+						    .cb = nljson_print_unsigned },
+	[BATADV_ATTR_BLA_CRC]			= { .name = "bla_crc",},
+};
+
 int netlink_create(struct state *state)
 {
 	int ret;
@@ -419,7 +464,7 @@ int netlink_print_common_cb(struct nl_msg *msg, void *arg)
 int netlink_print_common(struct state *state, char *orig_iface, int read_opt,
 			 float orig_timeout, float watch_interval,
 			 const char *header, uint8_t nl_cmd,
-			 nl_recvmsg_msg_cb_t callback)
+			 nl_recvmsg_msg_cb_t callback, uint8_t is_json)
 {
 	struct print_opts opts = {
 		.read_opt = read_opt,
@@ -427,6 +472,9 @@ int netlink_print_common(struct state *state, char *orig_iface, int read_opt,
 		.watch_interval = watch_interval,
 		.remaining_header = NULL,
 		.callback = callback,
+		.nl_cmd = nl_cmd,
+		.is_json = is_json,
+		.is_first = 1,
 	};
 	int hardifindex = 0;
 	struct nl_msg *msg;
@@ -495,6 +543,238 @@ int netlink_print_common(struct state *state, char *orig_iface, int read_opt,
 	return last_err;
 }
 
+void netlink_print_json_entries(struct nlattr *attrs[], int selected_attrs[],
+				int arr_len, struct print_opts *opts)
+{
+	const char *name;
+	uint8_t first_valid_attr = 1;
+	int idx, i;
+	struct json_opts json_opts = {
+		.use_alt_int_val = 0,
+		.alt_int_val = 0,
+	};
+
+
+	if (!opts->is_first)
+		printf(",");
+
+	for (i = 0; i < arr_len; i++) {
+		idx = selected_attrs[i];
+
+		if (attrs[idx] && batadv_netlink_policy_json[idx].cb) {
+			if (!first_valid_attr)
+				printf(",");
+			else
+				printf("{");
+
+			create_json_print_string(batadv_netlink_policy_json[idx].name,
+						 &name);
+			sanitize_string(name, strlen(name));
+			printf(":");
+			batadv_netlink_policy_json[idx].cb(&json_opts, opts,
+							   attrs, idx);
+
+			first_valid_attr = 0;
+		}
+	}
+	if (!first_valid_attr)
+		printf("}");
+}
+
+void nljson_print_str(struct json_opts *json_opts, struct print_opts *opts,
+		      struct nlattr *attrs[], int idx)
+{
+	(void) json_opts;
+	(void) opts;
+	const char *value;
+	const char *value_print;
+
+	value = nla_get_string(attrs[idx]);
+
+	create_json_print_string(value, &value_print);
+	sanitize_string(value_print, strlen(value_print));
+}
+
+void nljson_print_ifname_by_ifindex(struct json_opts *json_opts,
+				    struct print_opts *opts, struct nlattr *attrs[],
+				    int idx)
+{
+	(void) opts;
+	(void) json_opts;
+	char value[IF_NAMESIZE];
+	const char *value_buf;
+
+	if (!if_indextoname(nla_get_u32(attrs[idx]), value))
+		value[0] = '\0';
+
+	create_json_print_string(value, &value_buf);
+	sanitize_string(value_buf, strlen(value_buf));
+}
+
+void nljson_print_int(struct json_opts *json_opts, struct print_opts *opts,
+		      struct nlattr *attrs[], int idx)
+{
+	(void) opts;
+	int value;
+
+	if (json_opts->use_alt_int_val) {
+		json_opts->use_alt_int_val = 0;
+		value = json_opts->alt_int_val;
+	} else {
+		value = nla_get_u32(attrs[idx]);
+	}
+
+	printf("%d", value);
+}
+
+void nljson_print_unsigned(struct json_opts *json_opts, struct print_opts *opts,
+			   struct nlattr *attrs[], int idx)
+{
+	(void) json_opts;
+	(void) opts;
+	unsigned value = nla_get_u32(attrs[idx]);
+	printf("%u", value);
+}
+
+void nljson_print_uint8_t(struct json_opts *json_opts, struct print_opts *opts,
+			  struct nlattr *attrs[], int idx)
+{
+	(void) json_opts;
+	(void) opts;
+	uint8_t value = nla_get_u8(attrs[idx]);
+	printf("%u", value);
+}
+
+void nljson_print_mac(struct json_opts *json_opts, struct print_opts *opts,
+		      struct nlattr *attrs[], int idx)
+{
+	(void) json_opts;
+	(void) opts;
+	uint8_t *value = nla_data(attrs[idx]);
+	printf("\"%02x:%02x:%02x:%02x:%02x:%02x\"",
+		value[0], value[1], value[2], value[3], value[4], value[5]);
+}
+
+void nljson_print_bool(struct json_opts *json_opts, struct print_opts *opts,
+		       struct nlattr *attrs[], int idx)
+{
+	(void) json_opts;
+	(void) opts;
+	bool value = nla_get_flag(attrs[idx]);
+	printf("%s", BOOL_STRING(value));
+}
+
+void nljson_print_vid(struct json_opts *json_opts, struct print_opts *opts,
+		      struct nlattr *attrs[], int idx)
+{
+	(void) json_opts;
+	(void) opts;
+	int16_t value = nla_get_u16(attrs[idx]);
+	printf("%i", BATADV_PRINT_VID(value));
+}
+
+void nljson_print_crc32(struct json_opts *json_opts, struct print_opts *opts,
+		      struct nlattr *attrs[], int idx)
+{
+	(void) json_opts;
+	(void) opts;
+	int32_t value = nla_get_u32(attrs[idx]);
+	printf("\"0x%.8x\"", value);
+}
+
+void nljson_print_ttflags(struct json_opts *json_opts, struct print_opts *opts,
+			  struct nlattr *attrs[], int idx)
+{
+	const char *key_ls;
+	uint32_t flags;
+	char r, n, x, w, i, p, t;
+
+	flags = nla_get_u32(attrs[idx]);
+
+	r = '.', p = '.', n = '.', x = '.', w = '.', i = '.', t = '.';
+
+	if (opts->nl_cmd == BATADV_CMD_GET_TRANSTABLE_LOCAL) {
+		// last_seen = 0 when flags are set
+		json_opts->use_alt_int_val = 0;
+		json_opts->alt_int_val = 0;
+
+		if (flags & BATADV_TT_CLIENT_ROAM)
+			r = 'R';
+		if (flags & BATADV_TT_CLIENT_NEW)
+			n = 'N';
+		if (flags & BATADV_TT_CLIENT_PENDING)
+			x = 'X';
+		if (flags & BATADV_TT_CLIENT_WIFI)
+			w = 'W';
+		if (flags & BATADV_TT_CLIENT_ISOLA)
+			i = 'I';
+		if (flags & BATADV_TT_CLIENT_NOPURGE)  {
+			p = 'P';
+		} else {
+			if (!attrs[BATADV_ATTR_LAST_SEEN_MSECS]) {
+				fputs("Received invalid data from kernel.\n", stderr);
+				exit(1);
+			}
+			goto print_local_table;
+		}
+
+		json_opts->use_alt_int_val = 1;
+		goto print_local_table;
+	} else {
+		if (flags & BATADV_TT_CLIENT_ROAM)
+			r = 'R';
+		if (flags & BATADV_TT_CLIENT_WIFI)
+			w = 'W';
+		if (flags & BATADV_TT_CLIENT_ISOLA)
+			i = 'I';
+		if (flags & BATADV_TT_CLIENT_TEMP)
+			t = 'T';
+
+		goto print_global_table;
+	}
+
+print_local_table:
+	printf("\"%c%c%c%c%c%c\"", r, n, x, w, i, p);
+
+	printf(",");
+	create_json_print_string(batadv_netlink_policy_json[BATADV_ATTR_LAST_SEEN_MSECS].name,
+				 &key_ls);
+	sanitize_string(key_ls, strlen(key_ls));
+	printf(":");
+	nljson_print_int(json_opts, opts, attrs, BATADV_ATTR_LAST_SEEN_MSECS);
+
+	return;
+
+print_global_table:
+	printf("\"%c%c%c%c\"", r, w, i, t);
+}
+
+void create_json_print_string(const char *str, const char **str_ret)
+{
+	size_t needed = snprintf(NULL, 0, "\"%s\"", str);
+	char  *str_buf = malloc(needed+1);
+
+	sprintf(str_buf, "\"%s\"", str);
+
+	*str_ret = str_buf;
+}
+
+void sanitize_string(const char *str, int str_len)
+{
+	int i;
+
+	for (i = 0; i < str_len; i++) {
+		if (str[i] == '"')
+			printf("\"");
+		else if (str[i] == '\\')
+			printf("\\\\");
+		else if (!isprint(str[i]))
+			printf("\\x%02x", str[i]);
+		else
+			printf("%c", str[i]);
+	}
+}
+
 static int nlquery_error_cb(struct sockaddr_nl *nla __maybe_unused,
 			    struct nlmsgerr *nlerr, void *arg)
 {
diff --git a/netlink.h b/netlink.h
index 4ee2f39..c766741 100644
--- a/netlink.h
+++ b/netlink.h
@@ -21,8 +21,22 @@ struct print_opts {
 	float watch_interval;
 	nl_recvmsg_msg_cb_t callback;
 	char *remaining_header;
+	char *remaining_entry;
 	const char *static_header;
 	uint8_t nl_cmd;
+	uint8_t is_json;
+	uint8_t is_first;
+};
+
+struct json_opts {
+	uint8_t use_alt_int_val;
+	int alt_int_val;
+};
+
+struct nla_policy_json {
+	const char *name;
+	void (*cb)(struct json_opts *json_opts, struct print_opts *opts,
+		   struct nlattr *attrs[], int idx);
 };
 
 struct ether_addr;
@@ -40,13 +54,14 @@ int get_algoname_netlink(const char *mesh_iface, char *algoname,
 			 size_t algoname_len);
 
 extern struct nla_policy batadv_netlink_policy[];
+extern struct nla_policy_json batadv_netlink_policy_json[];
 
 int missing_mandatory_attrs(struct nlattr *attrs[], const int mandatory[],
 			    int num);
 int netlink_print_common(struct state *state, char *orig_iface, int read_opt,
 			 float orig_timeout, float watch_interval,
 			 const char *header, uint8_t nl_cmd,
-			 nl_recvmsg_msg_cb_t callback);
+			 nl_recvmsg_msg_cb_t callback, uint8_t is_json);
 
 int netlink_print_common_cb(struct nl_msg *msg, void *arg);
 int netlink_stop_callback(struct nl_msg *msg, void *arg);
@@ -54,6 +69,26 @@ int netlink_print_error(struct sockaddr_nl *nla, struct nlmsgerr *nlerr,
 			void *arg);
 void netlink_print_remaining_header(struct print_opts *opts);
 
+void netlink_print_json_entries(struct nlattr *attrs[], int selected_attrs[],
+				int arr_len, struct print_opts *opts);
+void netlink_print_json_entry(struct nlattr *attrs[], int idx);
+void nljson_print_str(struct json_opts *json_opts, struct print_opts *opts, struct nlattr *attrs[], int idx);
+void nljson_print_ifname_by_ifindex(struct json_opts *json_opts, struct print_opts *opts,
+				    struct nlattr *attrs[], int idx);
+void nljson_print_int(struct json_opts *json_opts, struct print_opts *opts, struct nlattr *attrs[], int idx);
+void nljson_print_unsigned(struct json_opts *json_opts, struct print_opts *opts, struct nlattr *attrs[],
+			   int idx);
+void nljson_print_uint8_t(struct json_opts *json_opts, struct print_opts *opts, struct nlattr *attrs[],
+			  int idx);
+void nljson_print_vid(struct json_opts *json_opts, struct print_opts *opts, struct nlattr *attrs[], int idx);
+void nljson_print_crc32(struct json_opts *json_opts, struct print_opts *opts, struct nlattr *attrs[], int idx);
+void nljson_print_mac(struct json_opts *json_opts, struct print_opts *opts, struct nlattr *attrs[], int idx);
+void nljson_print_bool(struct json_opts *json_opts, struct print_opts *opts, struct nlattr *attrs[], int idx);
+void nljson_print_ttflags(struct json_opts *json_opts, struct print_opts *opts, struct nlattr *attrs[],
+			  int idx);
+void create_json_print_string(const char *str, const char **str_ret);
+void sanitize_string(const char *str, int str_len);
+
 extern char algo_name_buf[256];
 extern int last_err;
 extern int64_t mcast_flags;
diff --git a/originators.c b/originators.c
index 8a29dd7..674656c 100644
--- a/originators.c
+++ b/originators.c
@@ -192,7 +192,7 @@ static int netlink_print_originators(struct state *state, char *orig_iface,
 	return netlink_print_common(state, orig_iface, read_opts,
 				    orig_timeout, watch_interval, header,
 				    BATADV_CMD_GET_ORIGINATORS,
-				    originators_callback);
+				    originators_callback, 0);
 }
 
 static struct debug_table_data batctl_debug_table_originators = {
diff --git a/transglobal.c b/transglobal.c
index 4eae95d..c9312c0 100644
--- a/transglobal.c
+++ b/transglobal.c
@@ -132,7 +132,7 @@ static int netlink_print_transglobal(struct state *state, char *orig_iface,
 				    orig_timeout, watch_interval,
 				    "   Client             VID Flags Last ttvn     Via        ttvn  (CRC       )\n",
 				    BATADV_CMD_GET_TRANSTABLE_GLOBAL,
-				    transglobal_callback);
+				    transglobal_callback, 0);
 }
 
 static struct debug_table_data batctl_debug_table_transglobal = {
diff --git a/translocal.c b/translocal.c
index a3ad3da..7753010 100644
--- a/translocal.c
+++ b/translocal.c
@@ -128,7 +128,7 @@ static int netlink_print_translocal(struct state *state, char *orig_iface,
 				    orig_timeout, watch_interval,
 				    "Client             VID Flags    Last seen (CRC       )\n",
 				    BATADV_CMD_GET_TRANSTABLE_LOCAL,
-				    translocal_callback);
+				    translocal_callback, 0);
 }
 
 static struct debug_table_data batctl_debug_table_translocal = {
