Skip to content

Commit

Permalink
fix #7489 feat(nimbus): Support bundled schemas in validation (#7490)
Browse files Browse the repository at this point in the history
* fix #7489 feat(nimbus): Support bundled schemas in validation

Because:

* The JSON Schema validation library we're using does not support
  relative refs (e.g., `{ "$ref": "#/$defs/foo" }`) and also does not
  support bundled schema (i.e., relative references under a sub-schema
  with an `$id`, see [this doc][1] for more details).

This commit:

* Adds a custom ref resolver that walks the provided schema and adds the
  top-level schema and all bundled schemas under `$defs` as known
  references.

This also requires the fix from
https://bugzilla.mozilla.org/show_bug.cgi?id=1778368 to land in
mozilla-central.

fixes #7489

[1]: https://json-schema.org/understanding-json-schema/structuring.html#bundling

* add tests

Co-authored-by: Jared Lockhart <[email protected]>
  • Loading branch information
brennie and jaredlockhart authored Jul 7, 2022
1 parent 1ab44e1 commit add8239
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 1 deletion.
17 changes: 16 additions & 1 deletion app/experimenter/experiments/api/v5/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,21 @@
from experimenter.outcomes import Outcomes


class NestedRefResolver(jsonschema.RefResolver):
"""A custom ref resolver that handles bundled schema."""

def __init__(self, schema):
super().__init__(base_uri=None, referrer=None)

if "$id" in schema:
self.store[schema["$id"]] = schema

if "$defs" in schema:
for dfn in schema["$defs"].values():
if "$id" in dfn:
self.store[dfn["$id"]] = dfn


class ExperimentNameValidatorMixin:
def validate_name(self, name):
if not (self.instance or name):
Expand Down Expand Up @@ -1003,7 +1018,7 @@ def validate_hypothesis(self, value):
def _validate_feature_value_against_schema(self, schema, value):
json_value = json.loads(value)
try:
jsonschema.validate(json_value, schema)
jsonschema.validate(json_value, schema, resolver=NestedRefResolver(schema))
except jsonschema.ValidationError as exc:
return [exc.message]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,37 @@
}
"""

REF_JSON_SCHEMA = """\
{
"$id": "resource://test.schema.json",
"$ref": "resource://test.schema.json#/$defs/Foo",
"$defs": {
"Foo": {
"$id": "file:///foo.schema.json",
"type": "object",
"properties": {
"bar": {
"$ref": "file:///foo.schema.json#/$defs/Bar"
}
},
"$defs": {
"Bar": {
"type": "object",
"properties": {
"baz": {
"type": "string"
},
"qux": {
"type": "integer"
}
}
}
}
}
}
}
"""


class TestNimbusReviewSerializerSingleFeature(TestCase):
maxDiff = None
Expand Down Expand Up @@ -956,6 +987,42 @@ def test_serializer_feature_config_validation_treatment_value_schema_warn(self):
serializer.warnings,
)

def test_serializer_feature_config_validation_supports_ref_json_schema(self):
experiment = NimbusExperimentFactory.create_with_lifecycle(
NimbusExperimentFactory.Lifecycles.CREATED,
status=NimbusExperiment.Status.DRAFT,
application=NimbusExperiment.Application.DESKTOP,
channel=NimbusExperiment.Channel.NO_CHANNEL,
warn_feature_schema=False,
feature_configs=[
NimbusFeatureConfigFactory.create(
schema=REF_JSON_SCHEMA,
application=NimbusExperiment.Application.DESKTOP,
)
],
)
reference_feature_value = experiment.reference_branch.feature_values.get()
reference_feature_value.value = """\
{
"bar": {
"baz": "baz",
"qux": 123
}
}
""".strip()
reference_feature_value.save()

serializer = NimbusReviewSerializer(
experiment,
data=NimbusReviewSerializer(
experiment,
context={"user": self.user},
).data,
context={"user": self.user},
)

self.assertTrue(serializer.is_valid())

def test_serializer_feature_config_validation_treatment_value_no_schema(self):
experiment = NimbusExperimentFactory.create_with_lifecycle(
NimbusExperimentFactory.Lifecycles.CREATED,
Expand Down

0 comments on commit add8239

Please sign in to comment.