diff --git a/tools/jsonschema2moindoc.py b/tools/jsonschema2moindoc.py
new file mode 100755
index 0000000000000000000000000000000000000000..4e20397ee1bbd9666e60bba8772adef0c6e6d5a4
--- /dev/null
+++ b/tools/jsonschema2moindoc.py
@@ -0,0 +1,172 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+from sys import argv, stderr
+from json import load, dumps
+from collections import Sequence, OrderedDict
+from urllib import unquote
+from pprint import pprint
+import re
+
+
+def flee(s):
+    print >>stderr, s
+    exit(1)
+
+
+def loadJSON(p):
+    try:
+        with open(str(p), "r") as f:
+            try:
+                json = load(f, object_pairs_hook=OrderedDict)
+            except ValueError, err:
+                flee ("%s: %s" % (p, err))
+    except IOError, err:
+        flee(err)
+
+    return json
+
+
+def resolve_ref(document, fragment):
+    """
+    Resolve $ref. Only local relative URIs are supported.
+    """
+    fragment = unquote(fragment).lstrip("#").lstrip("/")
+    parts = fragment.split("/") if fragment else []
+
+    for part in parts:
+        part = part.replace("~1", "/").replace("~0", "~")
+
+        if isinstance(document, Sequence):
+            # Array indexes should be turned into integers
+            try:
+                part = int(part)
+            except ValueError:
+                pass
+        try:
+            document = document[part]
+        except (TypeError, LookupError):
+            flee("Unresolvable JSON pointer: %r" % fragment)
+
+    return document
+
+
+def is_camel_cased(s):
+    return re.search('[a-z].*[A-Z].*[a-z]', s) is not None
+
+
+def process_simple(sch, name, parent, ordlist, card, whole, indent, res):
+    """
+    Generate Moin format entry
+    """
+
+    if "$ref" in sch and sch["$ref"].startswith("#/definitions/"):
+        mytype = sch["$ref"].split("/")[-1]
+        mytype = u"[[#" + mytype + u"|" + mytype + u"]]"
+    else:
+        mytype = sch["type"]
+        mytype = u"''" + mytype + u"''"
+
+    res["def"].append(
+        u"%(indent)s''%(nocamel)s%(name)s'': %(card)s%(type)s\n%(indent)s\t%(desc)s\n" % {
+            "indent": u"\t" * indent,
+            "name": name,
+            "type": mytype,
+            "card": u"array of " if card else u"",
+            "desc": sch["description"],
+            "nocamel": u"!" if is_camel_cased(name) else u""
+        }
+    )
+
+
+def recurse(sch, name, parent, ordlist, card, whole, indent, res):
+    """
+    Recurse into subtrees based on type.
+    Also crudely resolves references (by merging into current tree).
+    """
+    # Crudely resolve references (by merging into current tree)
+    if "$ref" in sch:
+        ref = resolve_ref(whole, sch["$ref"])
+        sch = dict(ref.items() + sch.items())
+
+    # Jumptable recursion
+    if "type" in sch:
+        fork[sch["type"]](sch, name, parent, ordlist, card, whole, indent, res)
+    else:
+        flee("Basic type not defined: %s", pprint(schema))
+
+
+def process_object(sch, name, parent, ordlist, card, whole, indent, res):
+    """
+    Process object type and recurse for children
+    """
+    # Generate data for non root objects only
+    if name:
+        process_simple(sch, name, parent, ordlist, card, whole, indent, res)
+
+    # Set ordinality list for direct subobject of this object
+    neword = sch["required"] if "required" in sch else []
+
+    # Process each child, with False cardinality
+    for n, subsch in sch["properties"].iteritems():
+        recurse(subsch, n, name, neword, False, whole, indent+1, res)
+
+
+def process_array(sch, name, parent, ordlist, card, whole, indent, res):
+    """
+    Process array type (recurse just for one "items" children, but indicate
+    cardinality
+    """
+    recurse(sch["items"], name, parent, ordlist, True, whole, indent, res)
+
+
+fork = {
+    "object": process_object,
+    "array": process_array,
+    "string": process_simple,
+    "number": process_simple,
+    "integer": process_simple,
+    "boolean": process_simple
+}
+
+
+def process_types(schema, res):
+    """
+    Process Types section
+    """
+    for t, vals in schema["definitions"].iteritems():
+        res["types"].append(
+            u"<<Anchor(%(name)s)>>\n=== %(name)s ===\n\n%(desc)s\n\n" % {
+                "name": t,
+                "desc": vals["description"]
+            }
+        )
+
+
+def main():
+    if len(argv)==2:
+        schp = argv[1]
+    else:
+        flee("Usage: %s <jsonschemafile>" % split(argv[0])[-1])
+
+    schema = loadJSON(schp)
+
+    res = {
+        "def": [],
+        "types": []
+    }
+    # First structure of JSON schema must be object
+    process_object(schema, None, "", [""], False, schema, -1, res)
+
+    process_types(schema, res)
+
+    print u"".join(
+        [schema["description"]] +
+        [u"\n\n== Definition ==\n\n"] +
+        res["def"] +
+        [u"\n<<Anchor(Types)>>\n== Types ==\n\n"] +
+        res["types"]
+    ).encode("utf-8")
+
+
+main()