This is an automated email from the ASF dual-hosted git repository.

beto pushed a commit to branch semantic-layer-ui-semantic-layer
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 4c36bbeab30c526716d4b5e5065b5f4bbe6989cf
Author: Beto Dealmeida <[email protected]>
AuthorDate: Fri Feb 13 19:04:50 2026 -0500

    Cleanup
---
 .../src/superset_core/semantic_layers/config.py    | 73 ++++++++++++++++++
 .../semantic_layers/semantic_layer.py              |  2 +
 superset/semantic_layers/api.py                    | 87 ++++++++++++++++++++--
 3 files changed, 157 insertions(+), 5 deletions(-)

diff --git a/superset-core/src/superset_core/semantic_layers/config.py 
b/superset-core/src/superset_core/semantic_layers/config.py
new file mode 100644
index 00000000000..c1b92a21008
--- /dev/null
+++ b/superset-core/src/superset_core/semantic_layers/config.py
@@ -0,0 +1,73 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+from typing import Any
+
+from pydantic import BaseModel
+
+
+def build_configuration_schema(
+    config_class: type[BaseModel],
+    configuration: BaseModel | None = None,
+) -> dict[str, Any]:
+    """
+    Build a JSON schema from a Pydantic configuration class.
+
+    Handles generic boilerplate that any semantic layer with dynamic fields 
needs:
+
+    - Reorders properties to match model field order (Pydantic sorts 
alphabetically)
+    - When ``configuration`` is None, sets ``enum: []`` on all ``x-dynamic`` 
properties
+      so the frontend renders them as empty dropdowns
+
+    Semantic layer implementations call this instead of 
``model_json_schema()`` directly,
+    then only need to add their own dynamic population logic.
+    """
+    schema = config_class.model_json_schema()
+
+    # Pydantic sorts properties alphabetically; restore model field order
+    field_order = [
+        field.alias or name
+        for name, field in config_class.model_fields.items()
+    ]
+    schema["properties"] = {
+        key: schema["properties"][key]
+        for key in field_order
+        if key in schema["properties"]
+    }
+
+    if configuration is None:
+        for prop_schema in schema["properties"].values():
+            if prop_schema.get("x-dynamic"):
+                prop_schema["enum"] = []
+
+    return schema
+
+
+def check_dependencies(
+    prop_schema: dict[str, Any],
+    configuration: BaseModel,
+) -> bool:
+    """
+    Check whether a dynamic property's dependencies are satisfied.
+
+    Reads the ``x-dependsOn`` list from the property schema and returns 
``True``
+    when every referenced attribute on ``configuration`` is truthy.
+    """
+    dependencies = prop_schema.get("x-dependsOn", [])
+    return all(getattr(configuration, dep, None) for dep in dependencies)
diff --git a/superset-core/src/superset_core/semantic_layers/semantic_layer.py 
b/superset-core/src/superset_core/semantic_layers/semantic_layer.py
index 615014f8c1b..4ab01d01842 100644
--- a/superset-core/src/superset_core/semantic_layers/semantic_layer.py
+++ b/superset-core/src/superset_core/semantic_layers/semantic_layer.py
@@ -33,6 +33,8 @@ class SemanticLayer(Protocol[ConfigT, SemanticViewT]):
     A protocol for semantic layers.
     """
 
+    configuration_class: type[BaseModel]
+
     @classmethod
     def from_configuration(
         cls,
diff --git a/superset/semantic_layers/api.py b/superset/semantic_layers/api.py
index a0a87e55081..bb94a0e4b91 100644
--- a/superset/semantic_layers/api.py
+++ b/superset/semantic_layers/api.py
@@ -24,6 +24,7 @@ from flask import make_response, request, Response
 from flask_appbuilder.api import expose, protect, safe
 from flask_appbuilder.models.sqla.interface import SQLAInterface
 from marshmallow import ValidationError
+from pydantic import ValidationError as PydanticValidationError
 
 from superset import event_logger
 from superset.commands.semantic_layer.create import CreateSemanticLayerCommand
@@ -73,6 +74,79 @@ def _serialize_layer(layer: SemanticLayer) -> dict[str, Any]:
     }
 
 
+def _infer_discriminators(
+    schema: dict[str, Any],
+    data: dict[str, Any],
+) -> dict[str, Any]:
+    """
+    Infer discriminator values for union fields when the frontend omits them.
+
+    Walks the schema's properties looking for discriminated unions (fields 
with a
+    ``discriminator.mapping``). For each one, tries to match the submitted data
+    against one of the variants by checking which variant's required fields are
+    present, then injects the discriminator value.
+    """
+    defs = schema.get("$defs", {})
+    for prop_name, prop_schema in schema.get("properties", {}).items():
+        value = data.get(prop_name)
+        if not isinstance(value, dict):
+            continue
+
+        # Find discriminated union via discriminator mapping
+        mapping = (
+            prop_schema.get("discriminator", {}).get("mapping")
+            if "discriminator" in prop_schema
+            else None
+        )
+        if not mapping:
+            continue
+
+        discriminator_field = prop_schema["discriminator"].get("propertyName")
+        if not discriminator_field or discriminator_field in value:
+            continue
+
+        # Try each variant: match by required fields present in the data
+        for disc_value, ref in mapping.items():
+            ref_name = ref.rsplit("/", 1)[-1] if "/" in ref else ref
+            variant_def = defs.get(ref_name, {})
+            required = set(variant_def.get("required", []))
+            # Exclude the discriminator itself from the check
+            required.discard(discriminator_field)
+            if required and required.issubset(value.keys()):
+                data = {
+                    **data,
+                    prop_name: {**value, discriminator_field: disc_value},
+                }
+                break
+
+    return data
+
+
+def _parse_partial_config(
+    cls: Any,
+    config: dict[str, Any],
+) -> Any:
+    """
+    Parse a partial configuration, handling discriminator inference and
+    falling back to lenient validation when strict parsing fails.
+    """
+    config_class = cls.configuration_class
+
+    # Infer discriminator values the frontend may have omitted
+    schema = config_class.model_json_schema()
+    config = _infer_discriminators(schema, config)
+
+    try:
+        return config_class.model_validate(config)
+    except (PydanticValidationError, ValueError):
+        pass
+
+    try:
+        return config_class.model_validate(config, context={"partial": True})
+    except (PydanticValidationError, ValueError):
+        return None
+
+
 class SemanticViewRestApi(BaseSupersetModelRestApi):
     datamodel = SQLAInterface(SemanticView)
 
@@ -224,12 +298,15 @@ class SemanticLayerRestApi(BaseSupersetApi):
 
         parsed_config = None
         if config := body.get("configuration"):
-            try:
-                parsed_config = cls.from_configuration(config).configuration
-            except Exception:  # pylint: disable=broad-except
-                parsed_config = None
+            parsed_config = _parse_partial_config(cls, config)
+
+        try:
+            schema = cls.get_configuration_schema(parsed_config)
+        except Exception:  # pylint: disable=broad-except
+            # Connection or query failures during schema enrichment should not
+            # prevent the form from rendering — return the base schema instead.
+            schema = cls.get_configuration_schema(None)
 
-        schema = cls.get_configuration_schema(parsed_config)
         resp = make_response(json.dumps({"result": schema}, sort_keys=False), 
200)
         resp.headers["Content-Type"] = "application/json; charset=utf-8"
         return resp

Reply via email to