Repository: scikit-hep/pyhf

Issue:
Skip schema validation for xml2json
### Summary

Sometimes upstream tools like HistFitter can produce non-valid XML workspaces, but we need to be able to convert them to JSON. Schema validation could be configured off in some of these cases (force the error to a warning instead).

### Additional Information

_No response_

### Code of Conduct

- [X] I agree to follow the Code of Conduct


Discussion:

Resolving patch:
diff --git a/src/pyhf/cli/rootio.py b/src/pyhf/cli/rootio.py
--- a/src/pyhf/cli/rootio.py
+++ b/src/pyhf/cli/rootio.py
@@ -29,7 +29,8 @@ def cli():
     default=None,
 )
 @click.option('--track-progress/--hide-progress', default=True)
-def xml2json(entrypoint_xml, basedir, output_file, track_progress):
+@click.option('--validation-as-error/--validation-as-warning', default=True)
+def xml2json(entrypoint_xml, basedir, output_file, track_progress, validation_as_error):
     """Entrypoint XML: The top-level XML file for the PDF definition."""
     try:
         import uproot
@@ -43,7 +44,12 @@ def xml2json(entrypoint_xml, basedir, output_file, track_progress):
         )
     from pyhf import readxml
 
-    spec = readxml.parse(entrypoint_xml, basedir, track_progress=track_progress)
+    spec = readxml.parse(
+        entrypoint_xml,
+        basedir,
+        track_progress=track_progress,
+        validation_as_error=validation_as_error,
+    )
     if output_file is None:
         click.echo(json.dumps(spec, indent=4, sort_keys=True))
     else:
diff --git a/src/pyhf/readxml.py b/src/pyhf/readxml.py
--- a/src/pyhf/readxml.py
+++ b/src/pyhf/readxml.py
@@ -1,5 +1,6 @@
 from pyhf import schema
 from pyhf import compat
+from pyhf import exceptions
 
 import logging
 
@@ -333,7 +334,7 @@ def dedupe_parameters(parameters):
     return list({v['name']: v for v in parameters}.values())
 
 
-def parse(configfile, rootdir, track_progress=False):
+def parse(configfile, rootdir, track_progress=False, validation_as_error=True):
     toplvl = ET.parse(configfile)
     inputs = tqdm.tqdm(
         [x.text for x in toplvl.findall('Input')],
@@ -366,8 +367,13 @@ def parse(configfile, rootdir, track_progress=False):
         ],
         'version': schema.version,
     }
-    schema.validate(result, 'workspace.json')
-
+    try:
+        schema.validate(result, 'workspace.json')
+    except exceptions.InvalidSpecification as exc:
+        if validation_as_error:
+            raise exc
+        else:
+            log.warning(exc)
     return result
 
 


Test patch:
diff --git a/tests/test_import.py b/tests/test_import.py
--- a/tests/test_import.py
+++ b/tests/test_import.py
@@ -6,6 +6,7 @@
 import pytest
 import xml.etree.ElementTree as ET
 import logging
+from jsonschema import ValidationError
 
 
 def assert_equal_dictionary(d1, d2):
@@ -448,3 +449,28 @@ def test_process_modifiers(mocker, caplog):
     assert {'name': 'staterror_myChannel', 'type': 'staterror', 'data': _err} in result[
         'modifiers'
     ]
+
+
+def test_import_validation_exception(mocker, caplog):
+
+    mocker.patch(
+        'pyhf.schema.validate',
+        side_effect=pyhf.exceptions.InvalidSpecification(
+            ValidationError('this is an invalid specification')
+        ),
+    )
+
+    with caplog.at_level(logging.WARNING, "pyhf.readxml"):
+        pyhf.readxml.parse(
+            'validation/xmlimport_input2/config/example.xml',
+            'validation/xmlimport_input2',
+            validation_as_error=False,
+        )
+        assert "this is an invalid specification" in caplog.text
+
+    with pytest.raises(pyhf.exceptions.InvalidSpecification):
+        pyhf.readxml.parse(
+            'validation/xmlimport_input2/config/example.xml',
+            'validation/xmlimport_input2',
+            validation_as_error=True,
+        )