Repository: scikit-hep/pyhf

Issue:
Option to skip workspace validation
### Summary

Workspace validation is currently not skippable:

https://github.com/scikit-hep/pyhf/blob/767ed59d06274f810e47944f1fe4c95d94968543/src/pyhf/workspace.py#L289-L298

In some scenarios the validation takes a significant amount of time, and it would be convenient if it was possible to skip it.

### Additional Information

<!--- Describe what you are showing in your example -->
This is using [NormalMeasurement_combined.txt](https://github.com/scikit-hep/pyhf/files/7522251/NormalMeasurement_combined.txt) from https://github.com/scikit-hep/pyhf/discussions/1695. While working with [this gist](https://gist.github.com/alexander-held/2f6727df4cc8f65d75368e293ca83b5a) used in https://github.com/scikit-hep/pyhf/discussions/1695#discussioncomment-1635955, I noticed that workspace validation can take a long time for this example. Below is a minimal reproducer:

<!--- and then paste your mock Python example code between the quotes below -->
```python (paste below)
import pyhf
import json

with open("NormalMeasurement_combined.txt") as f:
    ws = pyhf.Workspace(json.load(f))
```

This runs in around 5.4 seconds for me locally.

When commenting out 

https://github.com/scikit-hep/pyhf/blob/767ed59d06274f810e47944f1fe4c95d94968543/src/pyhf/workspace.py#L298

this time goes down to 1.6 seconds.

The workspace validation takes about as long as the model construction (via `ws.model()`), which for me takes about 4 seconds by itself (again dominated by validation time in this example). It is possible to skip model validation (via `validate=False`), and I believe it would be useful to allow the same for the workspace.

I also noticed that arbitrary kwargs can be passed to workspace construction, e.g. in the above example
```python
ws = pyhf.Workspace(json.load(f), abcdefg=False)
```
also runs fine. It may be convenient to catch these kwargs to help users identify typos.

### Code of Conduct

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


Discussion:
This seems reasonable to add with it being turned off by default so that only expert users will try to use it.

Resolving patch:
diff --git a/src/pyhf/pdf.py b/src/pyhf/pdf.py
--- a/src/pyhf/pdf.py
+++ b/src/pyhf/pdf.py
@@ -635,14 +635,21 @@ class Model:
     """The main pyhf model class."""
 
     def __init__(
-        self, spec, modifier_set=None, batch_size=None, validate=True, **config_kwargs
+        self,
+        spec,
+        modifier_set=None,
+        batch_size=None,
+        validate: bool = True,
+        **config_kwargs,
     ):
         """
         Construct a HistFactory Model.
 
         Args:
             spec (:obj:`jsonable`): The HistFactory JSON specification
-            batch_size (:obj:`None` or :obj:`int`): Number of simultaneous (batched) Models to compute.
+            batch_size (:obj:`None` or :obj:`int`): Number of simultaneous (batched)
+             Models to compute.
+            validate (:obj:`bool`): Whether to validate against a JSON schema
             config_kwargs: Possible keyword arguments for the model configuration
 
         Returns:
@@ -657,8 +664,8 @@ def __init__(
         self.schema = config_kwargs.pop('schema', 'model.json')
         self.version = config_kwargs.pop('version', None)
         # run jsonschema validation of input specification against the (provided) schema
-        log.info(f"Validating spec against schema: {self.schema:s}")
         if validate:
+            log.info(f"Validating spec against schema: {self.schema:s}")
             utils.validate(self.spec, self.schema, version=self.version)
         # build up our representation of the specification
         poi_name = config_kwargs.pop('poi_name', 'mu')
diff --git a/src/pyhf/workspace.py b/src/pyhf/workspace.py
--- a/src/pyhf/workspace.py
+++ b/src/pyhf/workspace.py
@@ -286,16 +286,28 @@ class Workspace(_ChannelSummaryMixin, dict):
 
     valid_joins = ['none', 'outer', 'left outer', 'right outer']
 
-    def __init__(self, spec, **config_kwargs):
-        """Workspaces hold the model, data and measurements."""
+    def __init__(self, spec, validate: bool = True, **config_kwargs):
+        """
+        Workspaces hold the model, data and measurements.
+
+        Args:
+            spec (:obj:`jsonable`): The HistFactory JSON specification
+            validate (:obj:`bool`): Whether to validate against a JSON schema
+            config_kwargs: Possible keyword arguments for the workspace configuration
+
+        Returns:
+            model (:class:`~pyhf.workspace.Workspace`): The Workspace instance
+
+        """
         spec = copy.deepcopy(spec)
         super().__init__(spec, channels=spec['channels'])
         self.schema = config_kwargs.pop('schema', 'workspace.json')
         self.version = config_kwargs.pop('version', spec.get('version', None))
 
         # run jsonschema validation of input specification against the (provided) schema
-        log.info(f"Validating spec against schema: {self.schema}")
-        utils.validate(self, self.schema, version=self.version)
+        if validate:
+            log.info(f"Validating spec against schema: {self.schema}")
+            utils.validate(self, self.schema, version=self.version)
 
         self.measurement_names = []
         for measurement in self.get('measurements', []):
@@ -784,7 +796,7 @@ def sorted(cls, workspace):
         return cls(newspec)
 
     @classmethod
-    def build(cls, model, data, name='measurement'):
+    def build(cls, model, data, name='measurement', validate: bool = True):
         """
         Build a workspace from model and data.
 
@@ -792,6 +804,7 @@ def build(cls, model, data, name='measurement'):
             model (~pyhf.pdf.Model): A model to store into a workspace
             data (:obj:`tensor`): A array holding observations to store into a workspace
             name (:obj:`str`): The name of the workspace measurement
+            validate (:obj:`bool`): Whether to validate against a JSON schema
 
         Returns:
             ~pyhf.workspace.Workspace: A new workspace object
@@ -823,4 +836,4 @@ def build(cls, model, data, name='measurement'):
             {'name': k, 'data': list(data[model.config.channel_slices[k]])}
             for k in model.config.channels
         ]
-        return cls(workspace)
+        return cls(workspace, validate=validate)


Test patch:
diff --git a/tests/test_workspace.py b/tests/test_workspace.py
--- a/tests/test_workspace.py
+++ b/tests/test_workspace.py
@@ -898,3 +898,14 @@ def test_wspace_unexpected_keyword_argument(simplemodels_model_data):
 
     with pytest.raises(pyhf.exceptions.Unsupported):
         pyhf.Workspace(spec, abc=True)
+
+
+def test_workspace_without_validation(mocker, simplemodels_model_data):
+    model, data = simplemodels_model_data
+
+    mocker.patch('pyhf.utils.validate')
+    ws = pyhf.Workspace.build(model, data, validate=False)
+    assert pyhf.utils.validate.called is False
+
+    pyhf.Workspace(dict(ws), validate=False)
+    assert pyhf.utils.validate.called is False