Repository: scikit-hep/pyhf

Issue:
Model specification is accessed before being validated
### Summary

The model specification is currently accessed before validation is done:
https://github.com/scikit-hep/pyhf/blob/a6186405ae5acee472133874e9a477dc41def7df/src/pyhf/mixins.py#L26-L37 

called via https://github.com/scikit-hep/pyhf/blob/a6186405ae5acee472133874e9a477dc41def7df/src/pyhf/workspace.py#L303-L310
(validation happens at the bottom).

If there is no strong reason for the order being like this, I think it would be useful to run the validation first to filter out problematic specifications.

I ran into this when building a multi-channel workspace from a single-channel workspace for debugging purposes, and ended up pasting the channel at the wrong level of indentation.

### OS / Environment

```console
n/a
```


### Steps to Reproduce

<!--- Paste your minimal failing Python example code between the quotes below -->
```python (paste below)
import pyhf

spec = {
    "channels": [
        {
            "name": "SR",
            "samples_wrong_name": []
        }
    ]
}

pyhf.Workspace(spec)
```

### File Upload (optional)

_No response_

### Expected Results

`pyhf.exceptions.InvalidSpecification` or similar raised via the schema validation

### Actual Results

```console
Traceback (most recent call last):
  File "[...]/test.py", line 12, in <module>
    pyhf.Workspace(spec)
  File "[...]/pyhf/src/pyhf/workspace.py", line 303, in __init__
    super().__init__(spec, channels=spec['channels'])
  File "[...]/pyhf/src/pyhf/mixins.py", line 28, in __init__
    self.channel_nbins[channel['name']] = len(channel['samples'][0]['data'])
KeyError: 'samples'
```


### pyhf Version

```console
pyhf, version 0.7.0rc2.dev18
```


### Code of Conduct

- [X] I agree to follow the Code of Conduct


Discussion:
I tried out this change to run validation first, which seems to pass relevant tests (I see some unrelated failures in e.g. `test_asymptotic_calculator_has_fitted_pars` where the tolerance may need to be increased). Happy to submit this as a PR if you think this makes sense.
```diff
diff --git a/src/pyhf/workspace.py b/src/pyhf/workspace.py
index 6339f71c..070f3966 100644
--- a/src/pyhf/workspace.py
+++ b/src/pyhf/workspace.py
@@ -300,14 +300,15 @@ def __init__(self, spec, validate: bool = True, **config_kwargs):

         """
         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
         if validate:
             log.info(f"Validating spec against schema: {self.schema}")
-            schema.validate(self, self.schema, version=self.version)
+            schema.validate(spec, self.schema, version=self.version)
+
+        super().__init__(spec, channels=spec['channels'])

         self.measurement_names = []
         for measurement in self.get('measurements', []):
```
> Happy to submit this as a PR if you think this makes sense.

Please do. :rocket: It would be great if you could also add a test that has the

> `pyhf.exceptions.InvalidSpecification` or similar raised via the schema validation

that you mentioned.

Also thanks for this great Issue!

Resolving patch:
diff --git a/src/pyhf/workspace.py b/src/pyhf/workspace.py
--- a/src/pyhf/workspace.py
+++ b/src/pyhf/workspace.py
@@ -300,14 +300,15 @@ def __init__(self, spec, validate: bool = True, **config_kwargs):
 
         """
         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
         if validate:
             log.info(f"Validating spec against schema: {self.schema}")
-            schema.validate(self, self.schema, version=self.version)
+            schema.validate(spec, self.schema, version=self.version)
+
+        super().__init__(spec, channels=spec['channels'])
 
         self.measurement_names = []
         for measurement in self.get('measurements', []):


Test patch:
diff --git a/tests/test_workspace.py b/tests/test_workspace.py
--- a/tests/test_workspace.py
+++ b/tests/test_workspace.py
@@ -910,3 +910,11 @@ def test_workspace_without_validation(mocker, simplemodels_model_data):
 
     pyhf.Workspace(dict(ws), validate=False)
     assert pyhf.schema.validate.called is False
+
+
+def test_workspace_invalid_specification():
+    spec = {"channels": [{"name": "SR", "samples_wrong_name": []}]}
+    # Ensure that an invalid specifications gets caught as such
+    # before a KeyError: 'samples' could be reached.
+    with pytest.raises(pyhf.exceptions.InvalidSpecification):
+        pyhf.Workspace(spec)