Skip to content

Commit a6c6c44

Browse files
committed
Fix add_extra_schema_types for oneOf
1 parent 6c66a2d commit a6c6c44

2 files changed

Lines changed: 86 additions & 0 deletions

File tree

src/dstack/_internal/utils/json_schema.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ def add_extra_schema_types(schema_property: dict, extra_types: list[dict]):
33
refs = [schema_property.pop("allOf")[0]]
44
elif "anyOf" in schema_property:
55
refs = schema_property.pop("anyOf")
6+
elif "oneOf" in schema_property:
7+
nested = {"oneOf": schema_property.pop("oneOf")}
8+
if "discriminator" in schema_property:
9+
nested["discriminator"] = schema_property.pop("discriminator")
10+
refs = [nested]
611
elif "type" in schema_property:
712
refs = [{"type": schema_property.pop("type")}]
813
else:
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import json
2+
3+
from dstack._internal.core.models.configurations import DstackConfiguration, ServiceConfiguration
4+
from dstack._internal.core.models.profiles import ProfilesConfig
5+
from dstack._internal.utils.json_schema import add_extra_schema_types
6+
7+
8+
class TestAddExtraSchemaTypes:
9+
def test_ref_becomes_any_of(self):
10+
prop = {"$ref": "#/definitions/Foo"}
11+
add_extra_schema_types(prop, extra_types=[{"type": "string"}])
12+
assert prop == {"anyOf": [{"$ref": "#/definitions/Foo"}, {"type": "string"}]}
13+
14+
def test_all_of_keeps_first_ref_only(self):
15+
prop = {"allOf": [{"$ref": "#/definitions/Foo"}]}
16+
add_extra_schema_types(prop, extra_types=[{"type": "integer"}])
17+
assert prop == {"anyOf": [{"$ref": "#/definitions/Foo"}, {"type": "integer"}]}
18+
19+
def test_any_of_is_extended_in_place(self):
20+
prop = {"anyOf": [{"type": "integer"}]}
21+
add_extra_schema_types(prop, extra_types=[{"type": "string"}])
22+
assert prop == {"anyOf": [{"type": "integer"}, {"type": "string"}]}
23+
24+
def test_type_is_wrapped(self):
25+
prop = {"type": "integer"}
26+
add_extra_schema_types(prop, extra_types=[{"type": "string"}])
27+
assert prop == {"anyOf": [{"type": "integer"}, {"type": "string"}]}
28+
29+
def test_other_keys_are_preserved(self):
30+
prop = {"title": "Model", "description": "d", "$ref": "#/definitions/Foo"}
31+
add_extra_schema_types(prop, extra_types=[{"type": "string"}])
32+
assert prop["title"] == "Model"
33+
assert prop["description"] == "d"
34+
35+
def test_discriminated_one_of_stays_grouped_with_its_discriminator(self):
36+
# A `Field(discriminator=...)` union renders as `oneOf` plus a sibling `discriminator`.
37+
# The two must move into the same `anyOf` member: a `discriminator` only applies to a
38+
# keyword whose every member carries the tag, so flattening the extra types in beside
39+
# the refs would produce an invalid schema.
40+
prop = {
41+
"title": "Model",
42+
"oneOf": [{"$ref": "#/definitions/Foo"}, {"$ref": "#/definitions/Bar"}],
43+
"discriminator": {"propertyName": "format", "mapping": {}},
44+
}
45+
add_extra_schema_types(prop, extra_types=[{"type": "string"}])
46+
assert prop == {
47+
"title": "Model",
48+
"anyOf": [
49+
{
50+
"oneOf": [{"$ref": "#/definitions/Foo"}, {"$ref": "#/definitions/Bar"}],
51+
"discriminator": {"propertyName": "format", "mapping": {}},
52+
},
53+
{"type": "string"},
54+
],
55+
}
56+
57+
def test_one_of_without_discriminator(self):
58+
prop = {"oneOf": [{"$ref": "#/definitions/Foo"}]}
59+
add_extra_schema_types(prop, extra_types=[{"type": "string"}])
60+
assert prop == {"anyOf": [{"oneOf": [{"$ref": "#/definitions/Foo"}]}, {"type": "string"}]}
61+
62+
63+
class TestSchemaGeneration:
64+
"""
65+
Guards the schemas CI generates and the docs build consumes. Nothing else in the suite
66+
exercises `schema_json()`, so a `schema_extra` hook that cannot handle the shape pydantic
67+
emits for a field fails only in CI.
68+
"""
69+
70+
def test_dstack_configuration_schema_is_generated(self):
71+
assert json.loads(DstackConfiguration.schema_json())["definitions"]
72+
73+
def test_profiles_config_schema_is_generated(self):
74+
assert json.loads(ProfilesConfig.schema_json())["definitions"]
75+
76+
def test_service_model_accepts_both_the_shorthand_and_the_tagged_forms(self):
77+
prop = json.loads(ServiceConfiguration.schema_json())["properties"]["model"]
78+
tagged, shorthand = prop["anyOf"]
79+
assert shorthand == {"type": "string"}
80+
assert tagged["discriminator"]["propertyName"] == "format"
81+
assert tagged["oneOf"]

0 commit comments

Comments
 (0)