Generate json schema from argparse CLI

Question:

I have a CLI written with argparse and I was wondering if there was a way to produce a JSON schema from the ArgumentParser? The thought behind this being to distribute the JSON schema to extensions interfacing with the application, thus removing the need for each extension to write and maintain their own schema.

My idea was to

  1. Convert the argparse.ArgumentParser to Python dictionary or JSON file
  2. and then pass that into a JSON schema generator

Example

import argparse
from genson import SchemaBuilder

parser = argparse.ArgumentParser(
    description="Some description", prog="myprog", usage="myprog [options]"
)
parser.add_argument(
    "-v",
    "--version",
    action="store_true",
    help="Print server version number and exit",
)
parser.add_argument(
    "-c",
    "--config",
    type=str,
    default=".fortls",
    help="Configuration options file (default file name: %(default)s)",
)
args = vars(parser.parse_args(""))
# Generate schema
builder = SchemaBuilder()
builder.add_schema({"type": "object", "properties": {}})
for k, v in args.items():
    builder.add_object({k: v})
print(builder.to_json(indent=2))

Output

{
  "$schema": "http://json-schema.org/schema#",
  "type": "object",
  "properties": {
    "version": {
      "type": "boolean"
    },
    "config": {
      "type": "string"
    }
  }
}

However, I quickly realised that calling vars(parser().parse_args("")) to convert the CLI into a dictionary resulted into a lot of information being lost, like descriptions and required.

Is there another way of doing this? I am open to swappingargparse with some other CLI if it would make generating a schema easier.

Additional resources

Tool to generate JSON schema from JSON data

Asked By: gnikit

||

Answers:

The solution that I came up with was to access the private variable _actions from ArgumentParser and convert that to a schema using pydantic. In my specific case it was quite easy to do since all the arguments in argparse were optional. If not, a bit more thought has to be put when creating the model with pydantic

from __future__ import annotations
from pydantic import Field, create_model
import argparse

parser = argparse.ArgumentParser(
    description="Some description", prog="myprog", usage="myprog [options]"
)
parser.add_argument(
    "-v",
    "--version",
    action="store_true",
    help="Print server version number and exit",
)
parser.add_argument(
    "-c",
    "--config",
    type=str,
    default=".fortls",
    help="Configuration options file (default file name: %(default)s)",
)

schema_vals = {}
for arg in parser._actions:
    # if condition for arguments to exclude:
    #     continue
    val = arg.default
    desc: str = arg.help.replace("%(default)s", str(val))  # type: ignore
    schema_vals[arg.dest] = Field(val, description=desc)  # type: ignore

m = create_model("MySchema", **schema_vals)
m.__doc__ = "Some description"

with open("schema.json", "w") as f:
    print(m.schema_json(indent=2), file=f)

Output

{
  "title": "MySchema",
  "description": "Some description",
  "type": "object",
  "properties": {
    "help": {
      "title": "Help",
      "description": "show this help message and exit",
      "default": "==SUPPRESS==",
      "type": "string"
    },
    "version": {
      "title": "Version",
      "description": "Print server version number and exit",
      "default": false,
      "type": "boolean"
    },
    "config": {
      "title": "Config",
      "description": "Configuration options file (default file name: .fortls)",
      "default": ".fortls",
      "type": "string"
    }
  }
}
Answered By: gnikit
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.