Skip to main content

A script to convert a WCAG-EM report from JSON to reStructuredText for a VPAT

At work, I've had to produce VPAT documents for the software I'm responsible for.

The VPAT template asks you to list, for each of the WCAG criteria, whether you support it or not, or if it doesn't apply.

The W3C have made a WCAG-EM Report Tool which helps you to work through the WCAG criteria and make notes about whether they're satisfied.

At the end, you can download a copy of the report in either summarised HTML format, or a JSON file with all the data you entered.

The first time I did a VPAT, I mostly manually converted the information from the WCAG-EM report to a reStructuredText table, to go in our Sphinx documentation.

Now I'm doing it a second time, I know I don't want to waste my time doing that!

So I've written a Python script which takes in the JSON file from the report tool and prints out the tables for the VPAT template, in reStructuredText format.

Here's my script:

2024/06/ (Source)

#!/usr/bin/env python
# coding: utf-8

# Copyright 2024 Christian Lawson-Perfect
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# See the License for the specific language governing permissions and
# limitations under the License.

# Convert a WCAG-EM report tool JSON file to reStructuredText
# This script converts the JSON file you get from the [WCAG-EM Report Tool]( into reStructuredText markup.
# It needs the following files:
# * - The data on WCAG criteria used by the report tool.
# * - The English readable names for WCAG criteria. Save as ``WCAG_text.json``.
# Usage:
#   python product-report.json

from collections import defaultdict
from itertools import groupby
import json
import sys

# Load the report data, and the WCAG criterion data

report_json_filename = sys.argv[1]

with open(report_json_filename) as f:
    data = json.load(f)

with open('wcag.json') as f:
    wcag = json.load(f)

with open('WCAG_text.json') as f:
    wcag_text = json.load(f)

wcag_ids = {v['id']: v for v in wcag['2.2'].values()}

criteria_names = {k: v['TITLE'] for k,v in wcag_text['SUCCESS_CRITERION'].items()}

criteria = defaultdict(list)

for item in data['auditSample']:
    id = item['test']['id'][len('WCAG22:'):]

# For each criterion, gather info from the report

report = []

def indent_lines(text, indent):
    lines = text.split('\n')
    return lines[0]+'\n'.join(indent+line for line in lines[1:])

for key, criterion in criteria.items():
    description = ''
    result = ''
    for item in criterion:
        if 'description' in item['result']:
            description = item['result']['description']

        if 'Website' not in item['subject']['type']:

        result = item['result']['outcome']['title']

    result_map = {
        'Passed': 'Supports',
        'Not present': 'Not Applicable',
    result = result_map.get(result, result)

    info = wcag_ids[key]
    num = info['num']
    conformanceLevel = info['conformanceLevel']
    name = criteria_names[num]

        'key': key,
        'description': indent_lines(description, ' '*6),
        'result': result,
        'num': num,
        'conformanceLevel': conformanceLevel,
        'name': name,

# Criteria not assessed
# The following criteria were not assessed:

print("The following criteria were not assessed:\n")
not_assessed = [n for n in wcag_ids.values() if n['id'] not in criteria]
for n in not_assessed:
    print(f'''* {n['num']} {n['id']} ({n['conformanceLevel']}){n['id']}''')


# Produce ReStructuredText of report

for level, items in groupby(sorted(report, key=lambda x: (x['conformanceLevel'], x['num'])), key=lambda x: x['conformanceLevel']):
    header = f'''Table 1: Success Criteria, Level {level}'''
.. list-table::
  :header-rows: 1


     - Criteria
     - Conformance Level
     - Remarks and Explanations''')

    for item in items:
        print('''  -
    - .. _vpat-{key}:

      `{num}: {name} <{key}>`__ (Level {conformanceLevel})
    - {result}
    - {description}'''.format(**item))


And it ends up looking something like this:

Table 1: Success Criteria, Level A


Conformance Level

Remarks and Explanations

1.1.1: Non-text Content (Level A)


Text is always the primary method of conveying information.

1.2.1: Audio-only and Video-only (Prerecorded) (Level A)

Not Applicable

1.2.2: Captions (Prerecorded) (Level A)

Not Applicable

1.2.3: Audio Description or Media Alternative (Prerecorded) (Level A)

Not Applicable

1.3.1: Info and Relationships (Level A)


1.3.2: Meaningful Sequence (Level A)


1.3.3: Sensory Characteristics (Level A)


All interactive elements are clearly labelled in text.

1.4.1: Use of Color (Level A)


and so on.

As ever, I'm putting this here both so I can find it again later, and in case anyone else finds it useful.