static JSON json_schema(std::set<std::string>* keys = 0)
{
    // Style: use all lower-case keys with no dashes or underscores.
    // Choose array or dictionary based on indexing. For example, we
    // use a dictionary for objects because we want to index by object
    // ID and an array for pages because we want to index by position.
    // The pages in the pages array contain references back to the
    // original object, which can be resolved in the objects
    // dictionary. When a PDF construct that maps back to an original
    // object is represented separately, use "object" as the key that
    // references the original object.

    // This JSON object doubles as a schema and as documentation for
    // our JSON output. Any schema mismatch is a bug in qpdf. This
    // helps to enforce our policy of consistently providing a known
    // structure where every documented key will always be present,
    // which makes it easier to consume our JSON. This is discussed in
    // more depth in the manual.
    JSON schema = JSON::makeDictionary();
    schema.addDictionaryMember(
        "version", JSON::makeString(
            "JSON format serial number; increased for non-compatible changes"));
    JSON j_params = schema.addDictionaryMember(
        "parameters", JSON::makeDictionary());
    j_params.addDictionaryMember(
        "decodelevel", JSON::makeString(
            "decode level used to determine stream filterability"));

    bool all_keys = ((keys == 0) || keys->empty());

    // The list of selectable top-level keys id duplicated in three
    // places: json_schema, do_json, and initOptionTable.
    if (all_keys || keys->count("objects"))
    {
        schema.addDictionaryMember(
            "objects", JSON::makeString(
                "dictionary of original objects;"
                " keys are 'trailer' or 'n n R'"));
    }
    if (all_keys || keys->count("pages"))
    {
        JSON page = schema.addDictionaryMember("pages", JSON::makeArray()).
            addArrayElement(JSON::makeDictionary());
        page.addDictionaryMember(
            "object",
            JSON::makeString("reference to original page object"));
        JSON image = page.addDictionaryMember("images", JSON::makeArray()).
            addArrayElement(JSON::makeDictionary());
        image.addDictionaryMember(
            "name",
            JSON::makeString("name of image in XObject table"));
        image.addDictionaryMember(
            "object",
            JSON::makeString("reference to image stream"));
        image.addDictionaryMember(
            "width",
            JSON::makeString("image width"));
        image.addDictionaryMember(
            "height",
            JSON::makeString("image height"));
        image.addDictionaryMember(
            "colorspace",
            JSON::makeString("color space"));
        image.addDictionaryMember(
            "bitspercomponent",
            JSON::makeString("bits per component"));
        image.addDictionaryMember("filter", JSON::makeArray()).
            addArrayElement(
                JSON::makeString("filters applied to image data"));
        image.addDictionaryMember("decodeparms", JSON::makeArray()).
            addArrayElement(
                JSON::makeString("decode parameters for image data"));
        image.addDictionaryMember(
            "filterable",
            JSON::makeString("whether image data can be decoded"
                             " using the decode level qpdf was invoked with"));
        page.addDictionaryMember("contents", JSON::makeArray()).
            addArrayElement(
                JSON::makeString("reference to each content stream"));
        page.addDictionaryMember(
            "label",
            JSON::makeString("page label dictionary, or null if none"));
        JSON outline = page.addDictionaryMember("outlines", JSON::makeArray()).
            addArrayElement(JSON::makeDictionary());
        outline.addDictionaryMember(
            "object",
            JSON::makeString("reference to outline that targets this page"));
        outline.addDictionaryMember(
            "title",
            JSON::makeString("outline title"));
        outline.addDictionaryMember(
            "dest",
            JSON::makeString("outline destination dictionary"));
        page.addDictionaryMember(
            "pageposfrom1",
            JSON::makeString("position of page in document numbering from 1"));
    }
    if (all_keys || keys->count("pagelabels"))
    {
        JSON labels = schema.addDictionaryMember(
            "pagelabels", JSON::makeArray()).
            addArrayElement(JSON::makeDictionary());
        labels.addDictionaryMember(
            "index",
            JSON::makeString("starting page position starting from zero"));
        labels.addDictionaryMember(
            "label",
            JSON::makeString("page label dictionary"));
    }
    if (all_keys || keys->count("outlines"))
    {
        JSON outlines = schema.addDictionaryMember(
            "outlines", JSON::makeArray()).
            addArrayElement(JSON::makeDictionary());
        outlines.addDictionaryMember(
            "object",
            JSON::makeString("reference to this outline"));
        outlines.addDictionaryMember(
            "title",
            JSON::makeString("outline title"));
        outlines.addDictionaryMember(
            "dest",
            JSON::makeString("outline destination dictionary"));
        outlines.addDictionaryMember(
            "kids",
            JSON::makeString("array of descendent outlines"));
        outlines.addDictionaryMember(
            "open",
            JSON::makeString("whether the outline is displayed expanded"));
        outlines.addDictionaryMember(
            "destpageposfrom1",
            JSON::makeString("position of destination page in document"
                             " numbered from 1; null if not known"));
    }
    if (all_keys || keys->count("acroform"))
    {
        JSON acroform = schema.addDictionaryMember(
            "acroform", JSON::makeDictionary());
        acroform.addDictionaryMember(
            "hasacroform",
            JSON::makeString("whether the document has interactive forms"));
        acroform.addDictionaryMember(
            "needappearances",
            JSON::makeString("whether the form fields' appearance"
                             " streams need to be regenerated"));
        JSON fields = acroform.addDictionaryMember(
            "fields", JSON::makeArray()).
            addArrayElement(JSON::makeDictionary());
        fields.addDictionaryMember(
            "object",
            JSON::makeString("reference to this form field"));
        fields.addDictionaryMember(
            "parent",
            JSON::makeString("reference to this field's parent"));
        fields.addDictionaryMember(
            "pageposfrom1",
            JSON::makeString("position of containing page numbered from 1"));
        fields.addDictionaryMember(
            "fieldtype",
            JSON::makeString("field type"));
        fields.addDictionaryMember(
            "fieldflags",
            JSON::makeString(
                "form field flags from /Ff --"
                " see pdf_form_field_flag_e in qpdf/Constants.h"));
        fields.addDictionaryMember(
            "fullname",
            JSON::makeString("full name of field"));
        fields.addDictionaryMember(
            "partialname",
            JSON::makeString("partial name of field"));
        fields.addDictionaryMember(
            "alternativename",
            JSON::makeString(
                "alternative name of field --"
                " this is the one usually shown to users"));
        fields.addDictionaryMember(
            "mappingname",
            JSON::makeString("mapping name of field"));
        fields.addDictionaryMember(
            "value",
            JSON::makeString("value of field"));
        fields.addDictionaryMember(
            "defaultvalue",
            JSON::makeString("default value of field"));
        fields.addDictionaryMember(
            "quadding",
            JSON::makeString(
                "field quadding --"
                " number indicating left, center, or right"));
        fields.addDictionaryMember(
            "ischeckbox",
            JSON::makeString("whether field is a checkbox"));
        fields.addDictionaryMember(
            "isradiobutton",
            JSON::makeString("whether field is a radio button --"
                             " buttons in a single group share a parent"));
        fields.addDictionaryMember(
            "ischoice",
            JSON::makeString("whether field is a list, combo, or dropdown"));
        fields.addDictionaryMember(
            "istext",
            JSON::makeString("whether field is a text field"));
        JSON j_choices = fields.addDictionaryMember(
            "choices",
            JSON::makeString("for choices fields, the list of"
                             " choices presented to the user"));
        JSON annotation = fields.addDictionaryMember(
            "annotation", JSON::makeDictionary());
        annotation.addDictionaryMember(
            "object",
            JSON::makeString("reference to the annotation object"));
        annotation.addDictionaryMember(
            "appearancestate",
            JSON::makeString("appearance state --"
                             " can be used to determine value for"
                             " checkboxes and radio buttons"));
        annotation.addDictionaryMember(
            "annotationflags",
            JSON::makeString(
                "annotation flags from /F --"
                " see pdf_annotation_flag_e in qpdf/Constants.h"));
    }
    return schema;
}