WIP
diff --git a/superset-frontend/src/components/Form/LabeledErrorBoundInput.tsx b/superset-frontend/src/components/Form/LabeledErrorBoundInput.tsx
index 5ef9024..0d92d4b 100644
--- a/superset-frontend/src/components/Form/LabeledErrorBoundInput.tsx
+++ b/superset-frontend/src/components/Form/LabeledErrorBoundInput.tsx
@@ -18,7 +18,7 @@
  */
 import { styled, css, SupersetTheme, t } from '@superset-ui/core';
 import { Tooltip } from 'src/components/Tooltip';
-import { Input } from 'src/components/Input';
+import { Input, InputNumber } from 'src/components/Input';
 import InfoTooltip from 'src/components/InfoTooltip';
 import { Icons } from 'src/components/Icons';
 import Button from 'src/components/Button';
@@ -46,6 +46,10 @@
   margin: ${({ theme }) => `${theme.gridUnit}px 0 ${theme.gridUnit * 2}px`};
 `;
 
+const StyledInputNumber = styled(InputNumber)`
+  margin: ${({ theme }) => `${theme.gridUnit}px 0 ${theme.gridUnit * 2}px`};
+`;
+
 const StyledInputPassword = styled(Input.Password)`
   margin: ${({ theme }) => `${theme.gridUnit}px 0 ${theme.gridUnit * 2}px`};
 `;
@@ -99,6 +103,48 @@
   }
 `;
 
+interface GetInputComponentProps {
+  name?: string;
+  type?: string;
+}
+
+const getInputComponent = (
+  props: GetInputComponentProps,
+  validationMethods: Record<string, unknown>,
+  visibilityToggle: boolean,
+): JSX.Element => {
+  if (visibilityToggle || props.name === 'password') {
+    return (
+      <StyledInputPassword
+        {...props}
+        {...validationMethods}
+        iconRender={visible =>
+          visible ? (
+            <Tooltip title={t('Hide password.')}>
+              <Icons.EyeInvisibleOutlined iconSize="m" css={iconReset} />
+            </Tooltip>
+          ) : (
+            <Tooltip title={t('Show password.')}>
+              <Icons.EyeOutlined
+                iconSize="m"
+                css={iconReset}
+                data-test="icon-eye"
+              />
+            </Tooltip>
+          )
+        }
+        role="textbox"
+      />
+    );
+  }
+
+  if (props.type === 'number') {
+    return <StyledInputNumber {...props} {...validationMethods} />;
+  }
+
+  return <StyledInput {...props} {...validationMethods} />;
+};
+
 const LabeledErrorBoundInput = ({
   label,
   validationMethods,
@@ -128,30 +174,7 @@
       help={errorMessage || helpText}
       hasFeedback={!!errorMessage}
     >
-      {visibilityToggle || props.name === 'password' ? (
-        <StyledInputPassword
-          {...props}
-          {...validationMethods}
-          iconRender={visible =>
-            visible ? (
-              <Tooltip title={t('Hide password.')}>
-                <Icons.EyeInvisibleOutlined iconSize="m" css={iconReset} />
-              </Tooltip>
-            ) : (
-              <Tooltip title={t('Show password.')}>
-                <Icons.EyeOutlined
-                  iconSize="m"
-                  css={iconReset}
-                  data-test="icon-eye"
-                />
-              </Tooltip>
-            )
-          }
-          role="textbox"
-        />
-      ) : (
-        <StyledInput {...props} {...validationMethods} />
-      )}
+      {getInputComponent(props, validationMethods, visibilityToggle)}
       {get_url && description ? (
         <Button
           type="link"
diff --git a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx
index 696f52b..c86aea0 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx
@@ -55,21 +55,19 @@
   validationErrors,
   db,
 }: FieldPropTypes) => (
-  <>
-    <ValidatedInput
-      id="port"
-      name="port"
-      type="number"
-      required={required}
-      value={db?.parameters?.port as number}
-      validationMethods={{ onBlur: getValidation }}
-      errorMessage={validationErrors?.port}
-      placeholder={t('e.g. 5432')}
-      className="form-group-w-50"
-      label={t('Port')}
-      onChange={changeMethods.onParametersChange}
-    />
-  </>
+  <ValidatedInput
+    id="port"
+    name="port"
+    type="number"
+    required={required}
+    value={db?.parameters?.port as number}
+    validationMethods={{ onBlur: getValidation }}
+    errorMessage={validationErrors?.port}
+    placeholder={t('e.g. 5432')}
+    className="form-group-w-50"
+    label={t('Port')}
+    onChange={changeMethods.onParametersChange}
+  />
 );
 export const httpPath = ({
   required,
diff --git a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/GenericField.tsx b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/GenericField.tsx
new file mode 100644
index 0000000..24a130e
--- /dev/null
+++ b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/GenericField.tsx
@@ -0,0 +1,127 @@
+/**
+ * import InfoTooltip from 'src/components/InfoTooltip'm
+ * 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.
+ */
+
+import { useEffect } from 'react';
+import { styled } from '@superset-ui/core';
+// eslint-disable-next-line no-restricted-imports
+import { SelectValue } from 'antd/lib/select';
+import InfoTooltip from 'src/components/InfoTooltip';
+import Select from 'src/components/Select/Select';
+import FormItem from 'src/components/Form/FormItem';
+import FormLabel from 'src/components/Form/FormLabel';
+import ValidatedInput from 'src/components/Form/LabeledErrorBoundInput';
+import { DatabaseParameters, FieldPropTypes } from '../../types';
+
+const StyledFormGroup = styled('div')`
+  input::-webkit-outer-spin-button,
+  input::-webkit-inner-spin-button {
+    -webkit-appearance: none;
+    margin: 0;
+  }
+  margin-bottom: ${({ theme }) => theme.gridUnit * 3}px;
+  .ant-form-item {
+    margin-bottom: 0;
+  }
+`;
+
+const StyledAlignment = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+const StyledFormLabel = styled(FormLabel)`
+  margin-bottom: 0;
+`;
+
+export const GenericField = ({
+  required,
+  changeMethods,
+  getValidation,
+  validationErrors,
+  db,
+  field,
+  parameter,
+}: FieldPropTypes) => {
+  // set default values
+  useEffect(() => {
+    if (!db?.parameters?.[field as keyof DatabaseParameters] && parameter?.default !== undefined) {
+      changeMethods.onParametersChange({
+        target: { name: field, value: parameter.default },
+      });
+    }
+  }, []);
+
+  const handleOptionChange = (value: SelectValue) => {
+    changeMethods.onParametersChange({
+      target: { name: field, value },
+    });
+  };
+
+  // enums are mapped to select inputs
+  if (parameter?.enum) {
+    return (
+      <StyledFormGroup>
+        <StyledAlignment>
+          <StyledFormLabel htmlFor={field} required={required}>
+            {parameter.title}
+          </StyledFormLabel>
+          {parameter?.description && (
+            <InfoTooltip tooltip={parameter.description} />
+          )}
+        </StyledAlignment>
+        <FormItem>
+          <Select
+            onChange={handleOptionChange}
+            options={parameter.enum.map((value: string) => ({
+              value,
+              label: value,
+            }))}
+            value={
+              db?.parameters?.[field as keyof DatabaseParameters] ||
+              parameter?.default
+            }
+          />
+        </FormItem>
+      </StyledFormGroup>
+    );
+  }
+
+  // text/number inputs
+  return (
+    <ValidatedInput
+      id={field}
+      name={field}
+      type={parameter.type === 'integer' ? 'number' : 'text'}
+      required={required}
+      value={
+        db?.parameters?.[field as keyof DatabaseParameters] ||
+        parameter?.default
+      }
+      validationMethods={{ onBlur: getValidation }}
+      errorMessage={validationErrors?.[field]}
+      placeholder={parameter?.['x-placeholder']}
+      helpText={parameter?.['x-help-text']}
+      label={parameter.title}
+      hasTooltip={!!parameter?.description}
+      tooltipText={parameter?.description}
+      onChange={changeMethods.onParametersChange}
+    />
+  );
+};
diff --git a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/index.tsx b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/index.tsx
index 0f375f3..19ca0d4 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/index.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/index.tsx
@@ -17,16 +17,25 @@
  * under the License.
  */
 import { SupersetTheme } from '@superset-ui/core';
+import { Col, Row } from 'src/components';
 import { Form } from 'src/components/Form';
-import { FormFieldOrder, FORM_FIELD_MAP } from './constants';
+import { FORM_FIELD_MAP } from './constants';
 import { formScrollableStyles, validatedFormStyles } from '../styles';
-import { DatabaseConnectionFormProps } from '../../types';
+import { DatabaseConnectionFormProps, ParameterFieldSchema } from '../../types';
+import { GenericField } from './GenericField';
+
+interface ParametersSchema {
+  order: string[];
+  properties: {
+    [key: string]: ParameterFieldSchema;
+  };
+  required?: string[];
+}
 
 const DatabaseConnectionForm = ({
   dbModel,
   db,
   editNewDb,
-  getPlaceholder,
   getValidation,
   isEditMode = false,
   onAddTableCatalog,
@@ -41,62 +50,63 @@
   validationErrors,
   clearValidationErrors,
 }: DatabaseConnectionFormProps) => {
-  const parameters = dbModel?.parameters as {
-    properties: {
-      [key: string]: {
-        default?: any;
-        description?: string;
-      };
-    };
-    required?: string[];
-  };
+  const parameters = dbModel?.parameters as ParametersSchema;
+
+  let orderedEntries = Object.entries(parameters?.properties || []).sort(
+    ([keyA], [keyB]) =>
+      parameters.order.indexOf(keyA) - parameters.order.indexOf(keyB),
+  );
+
+  // database name should come first
+  orderedEntries = [
+    ['database_name', { title: 'Name', type: 'string' }],
+    ...orderedEntries,
+  ];
+
+  const components = orderedEntries.map(([key, value]) => {
+    const FormComponent = FORM_FIELD_MAP[key] || GenericField;
+
+    return (
+      <Col span={(value?.['x-width'] || 1) * 24}>
+        {FormComponent({
+          required: parameters.required?.includes(key),
+          changeMethods: {
+            onParametersChange,
+            onChange,
+            onQueryChange,
+            onParametersUploadFileChange,
+            onAddTableCatalog,
+            onRemoveTableCatalog,
+            onExtraInputChange,
+            onEncryptedExtraInputChange,
+          },
+          validationErrors,
+          getValidation,
+          clearValidationErrors,
+          db,
+          key,
+          field: key,
+          isEditMode,
+          sslForced,
+          editNewDb,
+          parameter: value,
+        })}
+      </Col>
+    );
+  });
 
   return (
     <Form>
       <div
-        // @ts-ignore
         css={(theme: SupersetTheme) => [
           formScrollableStyles,
           validatedFormStyles(theme),
         ]}
       >
-        {parameters &&
-          FormFieldOrder.filter(
-            (key: string) =>
-              Object.keys(parameters.properties).includes(key) ||
-              key === 'database_name',
-          ).map(field =>
-            // @ts-ignore TODO: fix ComponentClass for SSHTunnelSwitchComponent not having call signature.
-            FORM_FIELD_MAP[field]({
-              required: parameters.required?.includes(field),
-              changeMethods: {
-                onParametersChange,
-                onChange,
-                onQueryChange,
-                onParametersUploadFileChange,
-                onAddTableCatalog,
-                onRemoveTableCatalog,
-                onExtraInputChange,
-                onEncryptedExtraInputChange,
-              },
-              validationErrors,
-              getValidation,
-              clearValidationErrors,
-              db,
-              key: field,
-              field,
-              default_value: parameters.properties[field]?.default,
-              description: parameters.properties[field]?.description,
-              isEditMode,
-              sslForced,
-              editNewDb,
-              placeholder: getPlaceholder ? getPlaceholder(field) : undefined,
-            }),
-          )}
+        <Row gutter={[4, 4]}>{components}</Row>
       </div>
     </Form>
   );
 };
-export const FormFieldMap = FORM_FIELD_MAP;
 
 export default DatabaseConnectionForm;
diff --git a/superset-frontend/src/features/databases/types.ts b/superset-frontend/src/features/databases/types.ts
index a3a87b1..ccd4f29 100644
--- a/superset-frontend/src/features/databases/types.ts
+++ b/superset-frontend/src/features/databases/types.ts
@@ -315,13 +315,12 @@
   clearValidationErrors: () => void;
   db?: DatabaseObject;
   dbModel?: DatabaseForm;
-  field: string;
-  default_value?: any;
-  description?: string;
   isEditMode?: boolean;
   sslForced?: boolean;
   defaultDBName?: string;
   editNewDb?: boolean;
+  field: string;
+  parameter: ParameterFieldSchema;
 }
 
 type ChangeMethodsType = FieldPropTypes['changeMethods'];
@@ -369,3 +368,15 @@
   clearValidationErrors: () => void;
   getPlaceholder?: (field: string) => string | undefined;
 }
+
+/* Type for a field in the DB engine spec parameters schema */
+export interface ParameterFieldSchema {
+  title: string;
+  type: string;
+  enum?: any[];
+  default?: any;
+  description?: string;
+  ['x-help-text']?: string;
+  ['x-placeholder']?: string;
+  ['x-width']?: number;
+}
diff --git a/superset/config.py b/superset/config.py
index 8be07ed..70087a5 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -1148,8 +1148,8 @@
 #         else:
 #             return f'tmp_{schema}'
 # Function accepts database object, user object, schema name and sql that will be run.
-SQLLAB_CTAS_SCHEMA_NAME_FUNC: (
-    None | (Callable[[Database, models.User, str, str], str])
+SQLLAB_CTAS_SCHEMA_NAME_FUNC: None | (
+    Callable[[Database, models.User, str, str], str]
 ) = None
 
 # If enabled, it can be used to store the results of long-running queries
@@ -1430,6 +1430,20 @@
 # e.g., DBS_AVAILABLE_DENYLIST: Dict[str, Set[str]] = {"databricks": {"pyhive", "pyodbc"}}  # noqa: E501
 DBS_AVAILABLE_DENYLIST: dict[str, set[str]] = {}
 
+# Allow Superset to use 3rd party DB engine specs, distributed in separate Python
+# packages, and registered under the `superset.db_engine_specs` entry point. Before
+# enabling this you should audit packages in your system that would be loaded, to
+# prevent a malicious package from injecting a DB engine spec that could steal database
+# credentials and have full access to your data. You can do this by running:
+#
+#   import importlib.metadata
+#
+#   entry_points = importlib.metadata.entry_points().get("superset.db_engine_specs", [])
+#   for ep in entry_points:
+#        print(ep.module)
+#
+ALLOW_3RD_PARTY_DB_ENGINE_SPECS = False
+
 # This auth provider is used by background (offline) tasks that need to access
 # protected resources. Can be overridden by end users in order to support
 # custom auth mechanisms
diff --git a/superset/constants.py b/superset/constants.py
index f60b79a..df29b98 100644
--- a/superset/constants.py
+++ b/superset/constants.py
@@ -243,3 +243,8 @@
     DEFAULT = "default"
     DATA = "data"
     THUMBNAIL = "thumbnail"
+
+
+DEFAULT_SQLALCHEMY_PLACEHOLDER = (
+    "engine+driver://user:password@host:port/dbname[?key1=value1&key2=value2...]"
+)
diff --git a/superset/databases/api.py b/superset/databases/api.py
index f879351..53cce2a 100644
--- a/superset/databases/api.py
+++ b/superset/databases/api.py
@@ -72,7 +72,10 @@
     NoValidFilesFoundError,
 )
 from superset.commands.importers.v1.utils import get_contents_from_bundle
-from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
+from superset.constants import (
+    MODEL_API_RW_METHOD_PERMISSION_MAP,
+    RouteMethod,
+)
 from superset.daos.database import DatabaseDAO
 from superset.databases.decorators import check_table_access
 from superset.databases.filters import DatabaseFilter, DatabaseUploadEnabledFilter
@@ -126,6 +129,7 @@
     get_username,
     parse_js_uri_path_item,
 )
+from superset.utils.database import parameters_json_schema
 from superset.utils.decorators import transaction
 from superset.utils.oauth2 import decode_oauth2_state
 from superset.utils.ssh_tunnel import mask_password_info
@@ -1896,14 +1900,10 @@
                 payload["default_driver"] = engine_spec.default_driver
 
             # show configuration parameters for DBs that support it
-            if (
-                hasattr(engine_spec, "parameters_json_schema")
-                and hasattr(engine_spec, "sqlalchemy_uri_placeholder")
-                and engine_spec.default_driver in drivers
-            ):
-                payload["parameters"] = engine_spec.parameters_json_schema()
-                payload["sqlalchemy_uri_placeholder"] = (
-                    engine_spec.sqlalchemy_uri_placeholder
+            if engine_spec.parameters_schema:
+                payload["parameters"] = parameters_json_schema(
+                    engine_spec.__name__,
+                    engine_spec.parameters_schema,
                 )
 
             available_databases.append(payload)
diff --git a/superset/databases/utils.py b/superset/databases/utils.py
index 526cf80..9d6f74d 100644
--- a/superset/databases/utils.py
+++ b/superset/databases/utils.py
@@ -118,13 +118,11 @@
     :param raw_url:
     :return:
     """
-
-    if isinstance(raw_url, str):
-        url = raw_url.strip()
-        try:
-            return make_url(url)  # noqa
-        except Exception as ex:
-            raise DatabaseInvalidError() from ex
-
-    else:
+    if not isinstance(raw_url, str):
         return raw_url
+
+    url = raw_url.strip()
+    try:
+        return make_url(url)  # noqa
+    except Exception as ex:
+        raise DatabaseInvalidError() from ex
diff --git a/superset/db_engine_specs/__init__.py b/superset/db_engine_specs/__init__.py
index 3c0911c..388c7cf 100644
--- a/superset/db_engine_specs/__init__.py
+++ b/superset/db_engine_specs/__init__.py
@@ -61,9 +61,26 @@
 def load_engine_specs() -> list[type[BaseEngineSpec]]:
     """
     Load all engine specs, native and 3rd party.
+
+    For context, DB engine specs can be installed from 3rd party Python packages via
+    entry points. This allows DB vendor to maintain their own engine specs in a release
+    cycle that's independent from Superset's.
+
+    These DB engine specs can replace the ones that come with Superset, by specifying
+    the `replaces` class attribute.
     """
     engine_specs: list[type[BaseEngineSpec]] = []
 
+    # load 3rd party engine specs first, so they have prioerity
+    if app.config["ALLOW_3RD_PARTY_DB_ENGINE_SPECS"]:
+        for ep in entry_points(group="superset.db_engine_specs"):
+            try:
+                engine_spec = ep.load()
+            except Exception:  # pylint: disable=broad-except
+                logger.warning("Unable to load Superset DB engine spec: %s", ep.name)
+                continue
+            engine_specs.append(engine_spec)
+
     # load standard engines
     db_engine_spec_dir = str(Path(__file__).parent)
     for module_info in pkgutil.iter_modules([db_engine_spec_dir], prefix="."):
@@ -73,14 +90,16 @@
             for attr in module.__dict__
             if is_engine_spec(getattr(module, attr))
         )
-    # load additional engines from external modules
-    for ep in entry_points(group="superset.db_engine_specs"):
-        try:
-            engine_spec = ep.load()
-        except Exception:  # pylint: disable=broad-except
-            logger.warning("Unable to load Superset DB engine spec: %s", ep.name)
-            continue
-        engine_specs.append(engine_spec)
+
+    # remove replaced engine specs
+    replaced = {
+        replaced_spec
+        for engine_spec in engine_specs
+        for replaced_spec in engine_spec.replaces
+    }
+    engine_specs = [
+        engine_spec for engine_spec in engine_specs if engine_spec not in replaced
+    ]
 
     return engine_specs
 
diff --git a/superset/db_engine_specs/base.py b/superset/db_engine_specs/base.py
index 79e0eb3..d727286 100644
--- a/superset/db_engine_specs/base.py
+++ b/superset/db_engine_specs/base.py
@@ -40,8 +40,6 @@
 import pandas as pd
 import requests
 import sqlparse
-from apispec import APISpec
-from apispec.ext.marshmallow import MarshmallowPlugin
 from deprecation import deprecated
 from flask import current_app, g, url_for
 from flask_appbuilder.security.sqla.models import User
@@ -60,7 +58,11 @@
 from sqlparse.tokens import CTE
 
 from superset import db, sql_parse
-from superset.constants import QUERY_CANCEL_KEY, TimeGrain as TimeGrainConstants
+from superset.constants import (
+    DEFAULT_SQLALCHEMY_PLACEHOLDER,
+    QUERY_CANCEL_KEY,
+    TimeGrain as TimeGrainConstants,
+)
 from superset.databases.utils import get_table_metadata, make_url_safe
 from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
 from superset.exceptions import DisallowedSQLFunction, OAuth2Error, OAuth2RedirectError
@@ -139,7 +141,9 @@
 }
 
 
-class TimestampExpression(ColumnClause):  # pylint: disable=abstract-method, too-many-ancestors
+class TimestampExpression(
+    ColumnClause
+):  # pylint: disable=abstract-method, too-many-ancestors
     def __init__(self, expr: str, col: ColumnClause, **kwargs: Any) -> None:
         """Sqlalchemy class that can be used to render native column elements respecting
         engine-specific quoting rules as part of a string-based expression.
@@ -212,11 +216,14 @@
     engine_aliases: set[str] = set()
     drivers: dict[str, str] = {}
     default_driver: str | None = None
+    sqlalchemy_uri_placeholder = DEFAULT_SQLALCHEMY_PLACEHOLDER
 
-    # placeholder with the SQLAlchemy URI template
-    sqlalchemy_uri_placeholder = (
-        "engine+driver://user:password@host:port/dbname[?key=value&key=value...]"
-    )
+    # 3rd-party DB engine specs can define this when they replace a built-in engine spec
+    replaces: set["BaseEngineSpec"] = set()
+
+    # schema of parameters needed to build the SQLAlchemy URI
+    parameters_schema: Schema | None = None
+    encryption_parameters: dict[str, str] = {}
 
     disable_ssh_tunneling = False
 
@@ -398,9 +405,9 @@
     max_column_name_length: int | None = None
     try_remove_schema_from_table_name = True  # pylint: disable=invalid-name
     run_multiple_statements_as_one = False
-    custom_errors: dict[
-        Pattern[str], tuple[str, SupersetErrorType, dict[str, Any]]
-    ] = {}
+    custom_errors: dict[Pattern[str], tuple[str, SupersetErrorType, dict[str, Any]]] = (
+        {}
+    )
 
     # List of JSON path to fields in `encrypted_extra` that should be masked when the
     # database is edited. By default everything is masked.
@@ -614,6 +621,46 @@
         return cls.allows_alias_in_select
 
     @classmethod
+    def build_sqlalchemy_uri(  # pylint: disable=unused-argument
+        cls,
+        parameters: dict[str, Any],
+        encrypted_extra: dict[str, str] | None = None,
+    ) -> str:
+        """
+        Method to build SQLAlchemy URI from discrete parameters.
+
+        This requires a Marshmallow schema to be set in the `parameters_schema` class
+        attribute.
+        """
+        raise NotImplementedError()
+
+    @classmethod
+    def get_parameters_from_uri(  # pylint: disable=unused-argument
+        cls,
+        uri: str,
+        encrypted_extra: dict[str, Any] | None = None,
+    ) -> dict[str, Any]:
+        """
+        Method to extract parameters from SQLAlchemy URI.
+
+        This is the opposite of `build_sqlalchemy_uri`.
+        """
+        raise NotImplementedError()
+
+    @classmethod
+    def validate_parameters(
+        cls,
+        properties: BasicPropertiesType,
+    ) -> list[SupersetError]:
+        """
+        Validates parameters.
+
+        See the `BasicParametersMixin` class for an example of progressive validation of
+        parameters.
+        """
+        return []
+
+    @classmethod
     def supports_url(cls, url: URL) -> bool:
         """
         Returns true if the DB engine spec supports a given SQLAlchemy URL.
@@ -2385,13 +2432,6 @@
     # schema describing the parameters used to configure the DB
     parameters_schema = BasicParametersSchema()
 
-    # recommended driver name for the DB engine spec
-    default_driver = ""
-
-    # query parameter to enable encryption in the database connection
-    # for Postgres this would be `{"sslmode": "verify-ca"}`, eg.
-    encryption_parameters: dict[str, str] = {}
-
     @classmethod
     def build_sqlalchemy_uri(  # pylint: disable=unused-argument
         cls,
@@ -2422,7 +2462,9 @@
 
     @classmethod
     def get_parameters_from_uri(  # pylint: disable=unused-argument
-        cls, uri: str, encrypted_extra: dict[str, Any] | None = None
+        cls,
+        uri: str,
+        encrypted_extra: dict[str, Any] | None = None,
     ) -> BasicParametersType:
         url = make_url_safe(uri)
         query = {
@@ -2497,6 +2539,8 @@
                     extra={"invalid": ["port"]},
                 ),
             )
+            return errors
+
         if not (isinstance(port, int) and 0 <= port < 2**16):
             errors.append(
                 SupersetError(
@@ -2519,20 +2563,3 @@
             )
 
         return errors
-
-    @classmethod
-    def parameters_json_schema(cls) -> Any:
-        """
-        Return configuration parameters as OpenAPI.
-        """
-        if not cls.parameters_schema:
-            return None
-
-        spec = APISpec(
-            title="Database Parameters",
-            version="1.0.0",
-            openapi_version="3.0.2",
-            plugins=[MarshmallowPlugin()],
-        )
-        spec.components.schema(cls.__name__, schema=cls.parameters_schema)
-        return spec.to_dict()["components"]["schemas"][cls.__name__]
diff --git a/superset/db_engine_specs/bigquery.py b/superset/db_engine_specs/bigquery.py
index c6c57b1..959b2ac 100644
--- a/superset/db_engine_specs/bigquery.py
+++ b/superset/db_engine_specs/bigquery.py
@@ -640,26 +640,6 @@
         return []
 
     @classmethod
-    def parameters_json_schema(cls) -> Any:
-        """
-        Return configuration parameters as OpenAPI.
-        """
-        if not cls.parameters_schema:
-            return None
-
-        spec = APISpec(
-            title="Database Parameters",
-            version="1.0.0",
-            openapi_version="3.0.0",
-            plugins=[ma_plugin],
-        )
-
-        ma_plugin.init_spec(spec)
-        ma_plugin.converter.add_attribute_function(encrypted_field_properties)
-        spec.components.schema(cls.__name__, schema=cls.parameters_schema)
-        return spec.to_dict()["components"]["schemas"][cls.__name__]
-
-    @classmethod
     def select_star(  # pylint: disable=too-many-arguments
         cls,
         database: Database,
diff --git a/superset/db_engine_specs/databricks.py b/superset/db_engine_specs/databricks.py
index 7574989..25d580f 100644
--- a/superset/db_engine_specs/databricks.py
+++ b/superset/db_engine_specs/databricks.py
@@ -415,22 +415,6 @@
             "encryption": encryption,
         }
 
-    @classmethod
-    def parameters_json_schema(cls) -> Any:
-        """
-        Return configuration parameters as OpenAPI.
-        """
-        if not cls.properties_schema:
-            return None
-
-        spec = APISpec(
-            title="Database Parameters",
-            version="1.0.0",
-            openapi_version="3.0.2",
-            plugins=[MarshmallowPlugin()],
-        )
-        spec.components.schema(cls.__name__, schema=cls.properties_schema)
-        return spec.to_dict()["components"]["schemas"][cls.__name__]
 
     @classmethod
     def get_default_catalog(
diff --git a/superset/db_engine_specs/duckdb.py b/superset/db_engine_specs/duckdb.py
index a5eba36..3a39844 100644
--- a/superset/db_engine_specs/duckdb.py
+++ b/superset/db_engine_specs/duckdb.py
@@ -171,23 +171,6 @@
 
         return errors
 
-    @classmethod
-    def parameters_json_schema(cls) -> Any:
-        """
-        Return configuration parameters as OpenAPI.
-        """
-        if not cls.parameters_schema:
-            return None
-
-        spec = APISpec(
-            title="Database Parameters",
-            version="1.0.0",
-            openapi_version="3.0.2",
-            plugins=[MarshmallowPlugin()],
-        )
-        spec.components.schema(cls.__name__, schema=cls.parameters_schema)
-        return spec.to_dict()["components"]["schemas"][cls.__name__]
-
 
 class DuckDBEngineSpec(DuckDBParametersMixin, BaseEngineSpec):
     engine = "duckdb"
diff --git a/superset/db_engine_specs/gsheets.py b/superset/db_engine_specs/gsheets.py
index 27416fc..176f6ec 100644
--- a/superset/db_engine_specs/gsheets.py
+++ b/superset/db_engine_specs/gsheets.py
@@ -206,26 +206,6 @@
         raise ValidationError("Invalid service credentials")
 
     @classmethod
-    def parameters_json_schema(cls) -> Any:
-        """
-        Return configuration parameters as OpenAPI.
-        """
-        if not cls.parameters_schema:
-            return None
-
-        spec = APISpec(
-            title="Database Parameters",
-            version="1.0.0",
-            openapi_version="3.0.0",
-            plugins=[ma_plugin],
-        )
-
-        ma_plugin.init_spec(spec)
-        ma_plugin.converter.add_attribute_function(encrypted_field_properties)
-        spec.components.schema(cls.__name__, schema=cls.parameters_schema)
-        return spec.to_dict()["components"]["schemas"][cls.__name__]
-
-    @classmethod
     def validate_parameters(
         cls,
         properties: GSheetsPropertiesType,
diff --git a/superset/db_engine_specs/snowflake.py b/superset/db_engine_specs/snowflake.py
index 5c9c42e..4be4e60 100644
--- a/superset/db_engine_specs/snowflake.py
+++ b/superset/db_engine_specs/snowflake.py
@@ -350,24 +350,6 @@
             )
         return errors
 
-    @classmethod
-    def parameters_json_schema(cls) -> Any:
-        """
-        Return configuration parameters as OpenAPI.
-        """
-        if not cls.parameters_schema:
-            return None
-
-        ma_plugin = MarshmallowPlugin()
-        spec = APISpec(
-            title="Database Parameters",
-            version="1.0.0",
-            openapi_version="3.0.0",
-            plugins=[ma_plugin],
-        )
-
-        spec.components.schema(cls.__name__, schema=cls.parameters_schema)
-        return spec.to_dict()["components"]["schemas"][cls.__name__]
 
     @staticmethod
     def update_params_from_encrypted_extra(
diff --git a/superset/models/core.py b/superset/models/core.py
index 9378452..86c9088 100755
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -127,7 +127,9 @@
     DYNAMIC_FORM = "dynamic_form"
 
 
-class Database(Model, AuditMixinNullable, ImportExportMixin):  # pylint: disable=too-many-public-methods
+class Database(
+    Model, AuditMixinNullable, ImportExportMixin
+):  # pylint: disable=too-many-public-methods
     """An ORM object that stores Database related information"""
 
     __tablename__ = "dbs"
@@ -306,16 +308,11 @@
         if (masked_encrypted_extra := self.masked_encrypted_extra) is not None:
             with suppress(TypeError, json.JSONDecodeError):
                 encrypted_config = json.loads(masked_encrypted_extra)
-        try:
-            # pylint: disable=useless-suppression
-            parameters = self.db_engine_spec.get_parameters_from_uri(  # type: ignore
-                masked_uri,
-                encrypted_extra=encrypted_config,
-            )
-        except Exception:  # pylint: disable=broad-except
-            parameters = {}
 
-        return parameters
+        return self.db_engine_spec.get_parameters_from_uri(
+            masked_uri,
+            encrypted_extra=encrypted_config,
+        )
 
     @property
     def parameters_schema(self) -> dict[str, Any]:
@@ -402,9 +399,7 @@
         return (
             username
             if (username := get_username())
-            else object_url.username
-            if self.impersonate_user
-            else None
+            else object_url.username if self.impersonate_user else None
         )
 
     @contextmanager
diff --git a/superset/utils/database.py b/superset/utils/database.py
index 719e7f2..a19fc9a 100644
--- a/superset/utils/database.py
+++ b/superset/utils/database.py
@@ -17,15 +17,20 @@
 from __future__ import annotations
 
 import logging
-from typing import TYPE_CHECKING
+from typing import Any, TYPE_CHECKING
 
+from apispec import APISpec
+from apispec.ext.marshmallow import MarshmallowPlugin
 from flask import current_app
 
 from superset.constants import EXAMPLES_DB_UUID
 
 if TYPE_CHECKING:
+    from marshmallow import Schema
+
     from superset.connectors.sqla.models import Database
 
+
 logging.getLogger("MARKDOWN").setLevel(logging.INFO)
 logger = logging.getLogger(__name__)
 
@@ -80,3 +85,28 @@
 
     db.session.delete(database)
     db.session.flush()
+
+
+def parameters_json_schema(
+    name: str,
+    parameters_schema: Schema | None,
+) -> dict[str, Any]:
+    """
+    Return configuration parameters as OpenAPI.
+    """
+    if not parameters_schema:
+        return {}
+
+    spec = APISpec(
+        title="Database Parameters",
+        version="1.0.0",
+        openapi_version="3.0.2",
+        plugins=[MarshmallowPlugin()],
+    )
+    spec.components.schema(name, schema=parameters_schema)
+    json_schema = spec.to_dict()["components"]["schemas"][name]
+
+    # preserve field order
+    json_schema["order"] = list(json_schema["properties"].keys())
+
+    return json_schema