diff --git a/ChangeLog b/ChangeLog
index 39a7e0e90dd6119342856bd3ed3a021b661f132d..14789d9024d4920d22249c7494c34a78dad4e23c 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,13 @@
 2022-05-30  Per Cederqvist  <ceder@lysator.liu.se>
 
+	checkargs: Generate protocol-a.json.
+	* doc/checkargs.py: Generate protocol-a.json. This is basically
+	the same info as in protocol-a-full.txt, but in a format that is
+	easier for a program to parse.  It is less easy for a human to
+	read, though.
+	* doc/Makefile.am (DISTCLEANFILES): Add protocol-a.json.
+	* doc/.gitignore: Ignore protocol-a.json.
+
 	checkargs: Simplify argument processing.
 	* doc/checkargs.py (lexer.__parse_request_arg): Append the
 	argument to the request object, so that the caller doesn't have to
diff --git a/doc/.gitignore b/doc/.gitignore
index 205306c9b14a646e105abae208a74259f91dcb01..bbbb64dd30bd081039f27df99132457eedbcd358 100644
--- a/doc/.gitignore
+++ b/doc/.gitignore
@@ -43,6 +43,7 @@ protocol-a.fns
 protocol-a.html
 protocol-a.info
 protocol-a.info-*
+protocol-a.json
 protocol-a.ky
 protocol-a.log
 protocol-a.pdf
diff --git a/doc/Makefile.am b/doc/Makefile.am
index 8733267a1cbf56ce695802a574d7f0dc55da0a4b..c53ab4bf68d7c184acd4b67e5c25aee99675935c 100644
--- a/doc/Makefile.am
+++ b/doc/Makefile.am
@@ -35,7 +35,7 @@ MOSTLYCLEANFILES = Protocol-A.notab lyskomd.notab *.tmp \
     Protocol-A.dvi
 
 DISTCLEANFILES = Protocol-A.pdf protocol-a-current.txt \
-	protocol-a-recommended.txt protocol-a-full.txt
+	protocol-a-recommended.txt protocol-a-full.txt protocol-a.json
 
 EXTRA_DIST = \
 	IDEAS \
diff --git a/doc/checkargs.py b/doc/checkargs.py
index 48f42d23605aec8d8e2d68265a1ae9dd6da07b2c..2d89f0322974cf3d3bb42a042b5a45722f3578f8 100644
--- a/doc/checkargs.py
+++ b/doc/checkargs.py
@@ -28,6 +28,7 @@
 
 import sys
 import types
+import json
 import re
 import os
 
@@ -1376,6 +1377,9 @@ class lexer:
                     typedef = prot_a_alternate(line, name, token)
                     token = self.__get_token()
                 else:
+                    if array:
+                        sys.stderr.write("unexpected ARRAY for simple\n")
+                        sys.exit(1)
                     typedef = prot_a_simple(line, name, array)
 
                 if token != ';':
@@ -2362,6 +2366,169 @@ def generate_summary_output(filename):
     fp.close()
     os.rename(filename + ".tmp", filename)
 
+def generate_json_output(filename):
+    m = {}
+    m["%%PROTOEDITION"] = set_values["PROTOEDITION"]
+    m["%%PROTOVER"] = set_values["PROTOVER"]
+    m["%%LYSKOMDVERSION"] = set_values["VERSION"]
+
+    tlist = list(tt.keys())
+    tlist.sort()
+
+    for tn in tlist:
+        if tn != tt[tn] and tn not in ["Conf-List-Archaic"]:
+            m.setdefault("type-alias", {})[tt[tn]] = tn
+
+    rlist = list(rt.keys())
+    rlist.sort()
+
+    for rn in rlist:
+        if rn != rt[rn] and rn not in ["lookup-name-1"]:
+            m.setdefault("request-alias", {})[rt[rn]] = rn
+
+    alist = list(at.keys())
+    alist.sort()
+
+    for an in alist:
+        if an != at[an]:
+            m.setdefault("async-alias", {})[at[an]] = an
+
+    tlist = list(tr.keys())
+    tlist.sort()
+
+    for tn in tlist:
+        if tn in ["Conf-List-Archaic-1"]:
+            continue
+        t = defined_types[tr[tn]]
+        if isinstance(t, prot_a_builtin):
+            m.setdefault("builtin", []).append(tn)
+        elif isinstance(t, prot_a_simple):
+            if t.array():
+                m.setdefault("array-types", {})[tn] = tt[t.base_type()]
+            else:
+                m.setdefault("simple-types", {})[tn] = tt[t.base_type()]
+        elif isinstance(t, prot_a_alternate):
+            m.setdefault("derived-types", {})[tn] = (
+                "union", tt[t.type_a()], tt[t.type_b()])
+        elif isinstance(t, prot_a_struct):
+            lst = []
+            for field_name, field_type, is_array in t.fields():
+                field = {
+                    'name': field_name,
+                    'type': field_type,
+                }
+                if is_array:
+                    field["array"] = True
+                lst.append(field)
+            m.setdefault("derived-types", {})[tn] = ("struct", lst)
+        elif isinstance(t, prot_a_bitstring):
+            m.setdefault("derived-types", {})[tn] = ("bitstring", t.bits())
+        elif isinstance(t, prot_a_selection):
+            alts = {}
+            for (nr, name, tailname, tailtype, array) in t.fields():
+                nr = int(nr)
+                alts[name] = {
+                    'tag': nr,
+                    'tailname': tailname,
+                    'type': tailtype,
+                }
+                if array:
+                    alts[name]["array"] = True
+            m.setdefault("derived-types", {})[tn] = ("selection", alts)
+        elif isinstance(t, prot_a_enumeration_of):
+            m.setdefault("derived-types", {})[tn] = ("enumeration-of",
+                                                     tt[t.base_type()])
+        else:
+            sys.stderr.write("bad type %s" % repr(t))
+            sys.exit(1)
+
+    for rn in rlist:
+        if rn in ["lookup-name-1"]:
+            continue
+        r = defined_request_names[rn]
+        info = {
+            'tag': int(r.request_nr()),
+            'name': rn,
+            'stable-name': rt[rn],
+            'protocol-version': int(r.protover()),
+        }
+        if r.recommended():
+            info["status"] = "recommended"
+        elif r.experimental():
+            info["status"] = "experimental"
+        elif r.obsolete():
+            info["status"] = "obsolete"
+            info["obsoleted-by"] = int(r.obsver())
+        else:
+            sys.stderr.write("No status found\n")
+            sys.exit(1)
+        info["documented-errors"] = r.error_codes()
+        args = []
+        for argname, argtype, argarray in r.arguments():
+            a = {
+                "name": argname,
+                "type": argtype,
+                "stable-type": tt[argtype],
+            }
+            if argarray:
+                a["array"] = True
+            args.append(a)
+        info["args"] = args
+        if r.return_type() is None:
+            info["return-type"] = None
+        else:
+            a = {
+                "type": r.return_type(),
+                "stable-type": tt[r.return_type()],
+            }
+            if r.array():
+                a["array"] = True
+            info["return-type"] = a
+        m.setdefault("requests", {})[rt[r.request_name()]] = info
+
+    alist = list(ar.keys())
+    alist.sort()
+
+    for an in alist:
+        r = defined_async_names[ar[an]]
+        info = {
+            'tag': int(r.request_nr()),
+            'name': ar[an],
+            'stable-name': an,
+            'protocol-version': int(r.protover()),
+        }
+        if r.recommended():
+            info["status"] = "recommended"
+        elif r.experimental():
+            info["status"] = "experimental"
+        elif r.obsolete():
+            info["status"] = "obsolete"
+            info["obsoleted-by"] = int(r.obsver())
+        else:
+            sys.stderr.write("No status found\n")
+            sys.exit(1)
+
+        args = []
+        for argname, argtype, argarray in r.arguments():
+            a = {
+                "name": argname,
+                "type": argtype,
+                "stable-type": tt[argtype],
+            }
+            if argarray:
+                a["array"] = True
+            args.append(a)
+        info["args"] = args
+        m.setdefault("asyncs", {})[an] = info
+
+    m["errors"] = defined_error_codes
+
+    fp = open(filename + ".tmp", "w")
+    fp.write(json.dumps(m, sort_keys=True, indent=2))
+    fp.close()
+    os.rename(filename + ".tmp", filename)
+    return
+
 if __name__ == '__main__':
     l = lexer(sys.argv[1])
     ret = l.run()
@@ -2369,6 +2536,7 @@ if __name__ == '__main__':
         generate_stable_output("protocol-a-full.txt", 0)
         generate_stable_output("protocol-a-recommended.txt", 1)
         generate_summary_output("protocol-a-current.txt")
+        generate_json_output("protocol-a.json")
     elif ret == 1:
         sys.exit(1)
     else: