-
Notifications
You must be signed in to change notification settings - Fork 2.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
anyOf/oneOf with Discriminated Unions + Null doesn't work #4380
Comments
Another point i've noticed is:
Example
"properties": {
"components": {
"discriminator": {
"mapping": {
"Logitech": "#/$defs/LogitechMouse",
"Razer": "#/$defs/RazerMouse"
},
"propertyName": "brand"
},
"oneOf": [
{
"$ref": "#/$defs/LogitechMouse"
},
{
"$ref": "#/$defs/RazerMouse"
}
],
"title": "AvailableMouses"
}
}
"properties": {
"components": {
"discriminator": {
"mapping": {
"Logitech": "#/$defs/LogitechMouse",
"Razer": "#/$defs/RazerMouse" <-------
},
"propertyName": "brand"
},
"oneOf": [
{
"$ref": "#/$defs/RazerMouse" <--------
},
{
"$ref": "#/$defs/LogitechMouse"
}
],
"title": "AvailableMouses"
}
} |
@guilhermedelyra Good job in finding a bug. Honestly I'm not sure how to fix it at this point. Maybe you have the time to debug through it all and provide a fix? |
oh, this was a journey haha but from what i understand, either rjsf or ajv-8 dont accept the 'mapping' part (at least that's what i got from the Console) so, for my use-case, i had to alter the way pydantic were generating the json-schema to use the 'dependencies' strategy instead. < not very proud of this, feels very hacky; but it works > from pydantic import BaseModel, GetJsonSchemaHandler
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import CoreSchema
class BaseJsonSchema(BaseModel):
@classmethod
def __get_pydantic_json_schema__( # noqa: C901
cls,
core_schema: CoreSchema,
handler: GetJsonSchemaHandler,
/,
) -> JsonSchemaValue:
json_schema = handler(core_schema)
def remove_discriminator_keys_from_definition(
schema: dict,
discriminator_keys: list[str],
) -> None:
if isinstance(schema, dict):
if "$ref" in schema:
actual_def = handler.resolve_ref_schema(schema)
for key in discriminator_keys:
actual_def["properties"].pop(key, None)
else:
leaf_discriminator_key = schema["required"][0]
for ref in schema["dependencies"][leaf_discriminator_key]["oneOf"]:
remove_discriminator_keys_from_definition(
ref,
discriminator_keys,
)
def process_schema(
schema: dict,
previous_discriminator_key: str | None = None,
discriminator_value: str | None = None,
) -> None:
if isinstance(schema, dict):
if "discriminator" in schema:
discriminator = schema["discriminator"]
property_name = discriminator["propertyName"]
mapping = discriminator["mapping"]
# Create the base schema
base_schema = {
"title": f"{property_name.capitalize()}",
"type": "object",
"properties": {
property_name: {
"type": "string",
"enum": list(mapping.keys()),
"title": property_name.capitalize(),
},
},
"required": [property_name],
"dependencies": {property_name: {"oneOf": []}},
}
if previous_discriminator_key:
base_schema["properties"][previous_discriminator_key] = { # type: ignore[index]
"type": "string",
"enum": [discriminator_value],
}
for key, sub_schema in mapping.items():
if isinstance(sub_schema, dict):
process_schema(sub_schema, property_name, key)
remove_discriminator_keys_from_definition(
sub_schema,
[previous_discriminator_key, property_name], # type: ignore[list-item]
)
elif isinstance(sub_schema, str):
sub_schema = {"$ref": sub_schema} # noqa: PLW2901
base_schema["dependencies"][property_name]["oneOf"].append( # type: ignore[index]
sub_schema,
)
schema.clear()
schema.update(base_schema)
for value in schema.values():
process_schema(value)
elif isinstance(schema, list):
for item in schema:
process_schema(item)
process_schema(json_schema)
return json_schema this way i went from this:
{
"$defs": {
"LogitechMouse": {
"additionalProperties": false,
"properties": {
"name": {
"default": "Logitech Mouse",
"title": "Name",
"type": "string"
},
"price": {
"default": 10.0,
"title": "Price",
"type": "number"
},
"type": {
"const": "peripheral",
"default": "peripheral",
"enum": [
"peripheral"
],
"title": "Type",
"type": "string"
},
"kind": {
"const": "mouse",
"default": "mouse",
"enum": [
"mouse"
],
"title": "Kind",
"type": "string"
},
"max_dpi": {
"default": 600,
"title": "Max Dpi",
"type": "integer"
},
"brand": {
"const": "Logitech",
"default": "Logitech",
"enum": [
"Logitech"
],
"title": "Brand",
"type": "string"
}
},
"title": "LogitechMouse",
"type": "object"
},
"RazerMouse": {
"additionalProperties": false,
"properties": {
"name": {
"default": "Razer Mouse",
"title": "Name",
"type": "string"
},
"price": {
"default": 20.0,
"title": "Price",
"type": "number"
},
"type": {
"const": "peripheral",
"default": "peripheral",
"enum": [
"peripheral"
],
"title": "Type",
"type": "string"
},
"kind": {
"const": "mouse",
"default": "mouse",
"enum": [
"mouse"
],
"title": "Kind",
"type": "string"
},
"max_dpi": {
"default": 1200,
"title": "Max Dpi",
"type": "integer"
},
"brand": {
"const": "Razer",
"default": "Razer",
"enum": [
"Razer"
],
"title": "Brand",
"type": "string"
}
},
"title": "RazerMouse",
"type": "object"
}
},
"properties": {
"components": {
"anyOf": [
{
"discriminator": {
"mapping": {
"Logitech": "#/$defs/LogitechMouse",
"Razer": "#/$defs/RazerMouse"
},
"propertyName": "brand"
},
"oneOf": [
{
"$ref": "#/$defs/LogitechMouse"
},
{
"$ref": "#/$defs/RazerMouse"
}
],
"title": "AvailableMouses"
},
{
"type": "null"
}
],
"title": "Components"
}
},
"required": [
"components"
],
"title": "Computer",
"type": "object"
} to this:
{
"$defs": {
"LogitechMouse": {
"additionalProperties": false,
"properties": {
"name": {
"default": "Logitech Mouse",
"title": "Name",
"type": "string"
},
"price": {
"default": 10.0,
"title": "Price",
"type": "number"
},
"type": {
"const": "peripheral",
"default": "peripheral",
"enum": [
"peripheral"
],
"title": "Type",
"type": "string"
},
"kind": {
"const": "mouse",
"default": "mouse",
"enum": [
"mouse"
],
"title": "Kind",
"type": "string"
},
"max_dpi": {
"default": 600,
"title": "Max Dpi",
"type": "integer"
},
"brand": {
"const": "Logitech",
"default": "Logitech",
"enum": [
"Logitech"
],
"title": "Brand",
"type": "string"
}
},
"title": "LogitechMouse",
"type": "object"
},
"RazerMouse": {
"additionalProperties": false,
"properties": {
"name": {
"default": "Razer Mouse",
"title": "Name",
"type": "string"
},
"price": {
"default": 20.0,
"title": "Price",
"type": "number"
},
"type": {
"const": "peripheral",
"default": "peripheral",
"enum": [
"peripheral"
],
"title": "Type",
"type": "string"
},
"kind": {
"const": "mouse",
"default": "mouse",
"enum": [
"mouse"
],
"title": "Kind",
"type": "string"
},
"max_dpi": {
"default": 1200,
"title": "Max Dpi",
"type": "integer"
},
"brand": {
"const": "Razer",
"default": "Razer",
"enum": [
"Razer"
],
"title": "Brand",
"type": "string"
}
},
"title": "RazerMouse",
"type": "object"
}
},
"properties": {
"components": {
"anyOf": [
{
"dependencies": {
"brand": {
"oneOf": [
{
"$ref": "#/$defs/LogitechMouse"
},
{
"$ref": "#/$defs/RazerMouse"
}
]
}
},
"properties": {
"brand": {
"enum": [
"Logitech",
"Razer"
],
"title": "Brand",
"type": "string"
}
},
"required": [
"brand"
],
"title": "Brand",
"type": "object"
},
{
"type": "null"
}
],
"title": "Components"
}
},
"required": [
"components"
],
"title": "Computer",
"type": "object"
} |
hopefully this helps, I encountered the same bug with a much simpler example: {
"properties": {
"username": {
"type": "string",
"maxLength": 20,
"title": "Username"
},
"name": {
"anyOf": [
{
"type": "string",
"maxLength": 50
},
{
"type": "null"
}
],
"title": "Name"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"username"
],
"title": "User"
} for the field |
This seems related to Also see my comment #4375 (comment) |
We've run into (I believe) the same issue. It appears to be a regression between releases (based on partial bisection via eg |
The regression is resolved if we revert the default value of However, this change yields different behavior for #4314 in that an excess property is erroneously preserved when switching between options. So this change alone may cause a different regression. |
Prerequisites
What theme are you using?
core
Version
5.x
Current Behavior
When using a JSON Schema that combines a discriminated union with null using anyOf, the form does not render the discriminated union options correctly. Instead, it only allows selecting the null option, and the expected fields for the other options are not displayed.
Expected Behavior
The form should correctly render the options from the discriminated union alongside the null option, allowing users to select any of the available types or null.
Steps To Reproduce
Failing Json Schema
playground
To prove that the
discriminated union
is not the problem, here's the same json without combining it with thenull
option:Working Json Schema
playground
Environment
Anything else?
Off-topic: I'm using
Pydantic
(version 2.9.2) to generate those Json-Schemas; here's the code:Pydantic Code
The text was updated successfully, but these errors were encountered: