diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py
index 328d020a56..ba36d261c6 100644
--- a/ironic/api/controllers/v1/node.py
+++ b/ironic/api/controllers/v1/node.py
@@ -17,7 +17,6 @@ import copy
 import datetime
 from http import client as http_client
 import json
-import os
 
 from ironic_lib import metrics_utils
 import jsonschema
@@ -123,62 +122,75 @@ ALLOWED_TARGET_POWER_STATES = (ir_states.POWER_ON,
 
 _NODE_DESCRIPTION_MAX_LENGTH = 4096
 
-with open(os.path.join(os.path.dirname(__file__),
-          'network-data-schema.json'), 'rb') as fl:
-    NETWORK_DATA_SCHEMA = json.load(fl)
+_NETWORK_DATA_SCHEMA = None
 
-NODE_SCHEMA = {
-    'type': 'object',
-    'properties': {
-        'automated_clean': {'type': ['string', 'boolean', 'null']},
-        'bios_interface': {'type': ['string', 'null']},
-        'boot_interface': {'type': ['string', 'null']},
-        'chassis_uuid': {'type': ['string', 'null']},
-        'conductor_group': {'type': ['string', 'null']},
-        'console_enabled': {'type': ['string', 'boolean', 'null']},
-        'console_interface': {'type': ['string', 'null']},
-        'deploy_interface': {'type': ['string', 'null']},
-        'description': {'type': ['string', 'null'],
-                        'maxLength': _NODE_DESCRIPTION_MAX_LENGTH},
-        'driver': {'type': 'string'},
-        'driver_info': {'type': ['object', 'null']},
-        'extra': {'type': ['object', 'null']},
-        'inspect_interface': {'type': ['string', 'null']},
-        'instance_info': {'type': ['object', 'null']},
-        'instance_uuid': {'type': ['string', 'null']},
-        'lessee': {'type': ['string', 'null']},
-        'management_interface': {'type': ['string', 'null']},
-        'maintenance': {'type': ['string', 'boolean', 'null']},
-        'name': {'type': ['string', 'null']},
-        'network_data': {'anyOf': [
-            {'type': 'null'},
-            {'type': 'object', 'additionalProperties': False},
-            NETWORK_DATA_SCHEMA
-        ]},
-        'network_interface': {'type': ['string', 'null']},
-        'owner': {'type': ['string', 'null']},
-        'power_interface': {'type': ['string', 'null']},
-        'properties': {'type': ['object', 'null']},
-        'raid_interface': {'type': ['string', 'null']},
-        'rescue_interface': {'type': ['string', 'null']},
-        'resource_class': {'type': ['string', 'null'], 'maxLength': 80},
-        'retired': {'type': ['string', 'boolean', 'null']},
-        'retired_reason': {'type': ['string', 'null']},
-        'storage_interface': {'type': ['string', 'null']},
-        'uuid': {'type': ['string', 'null']},
-        'vendor_interface': {'type': ['string', 'null']},
-    },
-    'required': ['driver'],
-    'additionalProperties': False,
-    'definitions': NETWORK_DATA_SCHEMA.get('definitions')
-}
 
-NODE_PATCH_SCHEMA = copy.deepcopy(NODE_SCHEMA)
-# add schema for patchable fields
-NODE_PATCH_SCHEMA['properties']['protected'] = {
-    'type': ['string', 'boolean', 'null']}
-NODE_PATCH_SCHEMA['properties']['protected_reason'] = {
-    'type': ['string', 'null']}
+def network_data_schema():
+    global _NETWORK_DATA_SCHEMA
+    if _NETWORK_DATA_SCHEMA is None:
+        with open(CONF.api.network_data_schema) as fl:
+            _NETWORK_DATA_SCHEMA = json.load(fl)
+    return _NETWORK_DATA_SCHEMA
+
+
+def node_schema():
+    network_data = network_data_schema()
+    return {
+        'type': 'object',
+        'properties': {
+            'automated_clean': {'type': ['string', 'boolean', 'null']},
+            'bios_interface': {'type': ['string', 'null']},
+            'boot_interface': {'type': ['string', 'null']},
+            'chassis_uuid': {'type': ['string', 'null']},
+            'conductor_group': {'type': ['string', 'null']},
+            'console_enabled': {'type': ['string', 'boolean', 'null']},
+            'console_interface': {'type': ['string', 'null']},
+            'deploy_interface': {'type': ['string', 'null']},
+            'description': {'type': ['string', 'null'],
+                            'maxLength': _NODE_DESCRIPTION_MAX_LENGTH},
+            'driver': {'type': 'string'},
+            'driver_info': {'type': ['object', 'null']},
+            'extra': {'type': ['object', 'null']},
+            'inspect_interface': {'type': ['string', 'null']},
+            'instance_info': {'type': ['object', 'null']},
+            'instance_uuid': {'type': ['string', 'null']},
+            'lessee': {'type': ['string', 'null']},
+            'management_interface': {'type': ['string', 'null']},
+            'maintenance': {'type': ['string', 'boolean', 'null']},
+            'name': {'type': ['string', 'null']},
+            'network_data': {'anyOf': [
+                {'type': 'null'},
+                {'type': 'object', 'additionalProperties': False},
+                network_data
+            ]},
+            'network_interface': {'type': ['string', 'null']},
+            'owner': {'type': ['string', 'null']},
+            'power_interface': {'type': ['string', 'null']},
+            'properties': {'type': ['object', 'null']},
+            'raid_interface': {'type': ['string', 'null']},
+            'rescue_interface': {'type': ['string', 'null']},
+            'resource_class': {'type': ['string', 'null'], 'maxLength': 80},
+            'retired': {'type': ['string', 'boolean', 'null']},
+            'retired_reason': {'type': ['string', 'null']},
+            'storage_interface': {'type': ['string', 'null']},
+            'uuid': {'type': ['string', 'null']},
+            'vendor_interface': {'type': ['string', 'null']},
+        },
+        'required': ['driver'],
+        'additionalProperties': False,
+        'definitions': network_data.get('definitions', {})
+    }
+
+
+def node_patch_schema():
+    node_patch = copy.deepcopy(node_schema())
+    # add schema for patchable fields
+    node_patch['properties']['protected'] = {
+        'type': ['string', 'boolean', 'null']}
+    node_patch['properties']['protected_reason'] = {
+        'type': ['string', 'null']}
+    return node_patch
+
 
 NODE_VALIDATE_EXTRA = args.dict_valid(
     automated_clean=args.boolean,
@@ -191,15 +203,30 @@ NODE_VALIDATE_EXTRA = args.dict_valid(
     uuid=args.uuid,
 )
 
-NODE_VALIDATOR = args.and_valid(
-    args.schema(NODE_SCHEMA),
-    NODE_VALIDATE_EXTRA
-)
 
-NODE_PATCH_VALIDATOR = args.and_valid(
-    args.schema(NODE_PATCH_SCHEMA),
-    NODE_VALIDATE_EXTRA
-)
+_NODE_VALIDATOR = None
+_NODE_PATCH_VALIDATOR = None
+
+
+def node_validator(name, value):
+    global _NODE_VALIDATOR
+    if _NODE_VALIDATOR is None:
+        _NODE_VALIDATOR = args.and_valid(
+            args.schema(node_schema()),
+            NODE_VALIDATE_EXTRA
+        )
+    return _NODE_VALIDATOR(name, value)
+
+
+def node_patch_validator(name, value):
+    global _NODE_PATCH_VALIDATOR
+    if _NODE_PATCH_VALIDATOR is None:
+        _NODE_PATCH_VALIDATOR = args.and_valid(
+            args.schema(node_patch_schema()),
+            NODE_VALIDATE_EXTRA
+        )
+    return _NODE_PATCH_VALIDATOR(name, value)
+
 
 PATCH_ALLOWED_FIELDS = [
     'automated_clean',
@@ -334,7 +361,7 @@ def validate_network_data(network_data):
     :raises: Invalid if network data is not schema-compliant
     """
     try:
-        jsonschema.validate(network_data, NETWORK_DATA_SCHEMA)
+        jsonschema.validate(network_data, network_data_schema())
 
     except json_schema_exc.ValidationError as e:
         # NOTE: Even though e.message is deprecated in general, it is
@@ -1838,7 +1865,7 @@ class NodesController(rest.RestController):
         api_utils.patch_update_changed_fields(
             node, rpc_node,
             fields=set(objects.Node.fields) - {'traits'},
-            schema=NODE_PATCH_SCHEMA,
+            schema=node_patch_schema(),
             id_map={'chassis_id': chassis and chassis.id or None}
         )
 
@@ -2098,7 +2125,7 @@ class NodesController(rest.RestController):
     @METRICS.timer('NodesController.post')
     @method.expose(status_code=http_client.CREATED)
     @method.body('node')
-    @args.validate(node=NODE_VALIDATOR)
+    @args.validate(node=node_validator)
     def post(self, node):
         """Create a new node.
 
@@ -2335,7 +2362,7 @@ class NodesController(rest.RestController):
         node_dict = api_utils.apply_jsonpatch(node_dict, patch)
 
         api_utils.patched_validate_with_schema(
-            node_dict, NODE_PATCH_SCHEMA, NODE_PATCH_VALIDATOR)
+            node_dict, node_patch_schema(), node_patch_validator)
 
         self._update_changed_fields(node_dict, rpc_node)
         # NOTE(tenbrae): we calculate the rpc topic here in case node.driver
diff --git a/ironic/conf/api.py b/ironic/conf/api.py
index ba23627b08..dcf235eddc 100644
--- a/ironic/conf/api.py
+++ b/ironic/conf/api.py
@@ -66,6 +66,10 @@ opts = [
                default=300,
                mutable=True,
                help=_('Maximum interval (in seconds) for agent heartbeats.')),
+    cfg.StrOpt(
+        'network_data_schema',
+        default='$pybasedir/api/controllers/v1/network-data-schema.json',
+        help=_("Schema for network data used by this deployment.")),
 ]
 
 opt_group = cfg.OptGroup(name='api',
diff --git a/ironic/tests/unit/api/base.py b/ironic/tests/unit/api/base.py
index ebf89df32a..6670be14e3 100644
--- a/ironic/tests/unit/api/base.py
+++ b/ironic/tests/unit/api/base.py
@@ -27,6 +27,7 @@ from oslo_config import cfg
 import pecan
 import pecan.testing
 
+from ironic.api.controllers.v1 import node as api_node
 from ironic.tests.unit.db import base as db_base
 
 PATH_PREFIX = '/v1'
@@ -65,6 +66,10 @@ class BaseApiTest(db_base.DbTestCase):
         self._check_version = p.start()
         self.addCleanup(p.stop)
 
+        api_node._NETWORK_DATA_SCHEMA = None
+        api_node._NODE_VALIDATOR = None
+        api_node._NODE_PATCH_VALIDATOR = None
+
     def _make_app(self):
         # Determine where we are so we can set up paths in the config
         root_dir = self.path_get()
diff --git a/ironic/tests/unit/api/controllers/v1/test_node.py b/ironic/tests/unit/api/controllers/v1/test_node.py
index 185e8f27e1..302da10551 100644
--- a/ironic/tests/unit/api/controllers/v1/test_node.py
+++ b/ironic/tests/unit/api/controllers/v1/test_node.py
@@ -17,6 +17,7 @@ import datetime
 from http import client as http_client
 import json
 import os
+import tempfile
 from unittest import mock
 from urllib import parse as urlparse
 
@@ -3807,6 +3808,47 @@ class TestPatch(test_api_base.BaseApiTest):
         self.assertEqual('application/json', response.content_type)
         self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
 
+    def test_update_network_data_wrong_format(self):
+        node = obj_utils.create_test_node(self.context,
+                                          uuid=uuidutils.generate_uuid())
+        self.mock_update_node.return_value = node
+        headers = {api_base.Version.string: '1.66'}
+
+        response = self.patch_json('/nodes/%s' % node.uuid,
+                                   [{'path': '/network_data',
+                                     'value': {'cat': 'meow'},
+                                     'op': 'replace'}],
+                                   headers=headers,
+                                   expect_errors=True)
+        self.assertEqual('application/json', response.content_type)
+        self.assertEqual(http_client.BAD_REQUEST, response.status_code)
+
+    def test_update_network_data_custom(self):
+        custom_schema = {
+            'type': 'object',
+            'properties': {
+                'cat': {'type': 'string'},
+            },
+        }
+        with tempfile.NamedTemporaryFile('wt') as fp:
+            json.dump(custom_schema, fp)
+            fp.flush()
+            self.config(network_data_schema=fp.name, group='api')
+
+            node = obj_utils.create_test_node(self.context,
+                                              uuid=uuidutils.generate_uuid(),
+                                              provision_state='active')
+            self.mock_update_node.return_value = node
+            headers = {api_base.Version.string: '1.66'}
+
+            response = self.patch_json('/nodes/%s' % node.uuid,
+                                       [{'path': '/network_data',
+                                         'value': {'cat': 'meow'},
+                                         'op': 'replace'}],
+                                       headers=headers)
+            self.assertEqual('application/json', response.content_type)
+            self.assertEqual(http_client.OK, response.status_code)
+
     @mock.patch.object(api_utils, 'check_multiple_node_policies_and_retrieve',
                        autospec=True)
     def test_patch_policy_update(self, mock_cmnpar):
diff --git a/ironic/tests/unit/api/utils.py b/ironic/tests/unit/api/utils.py
index d3963fc15a..9be350e8b5 100644
--- a/ironic/tests/unit/api/utils.py
+++ b/ironic/tests/unit/api/utils.py
@@ -111,7 +111,7 @@ def node_post_data(**kw):
             node.pop(field, None)
 
     return remove_other_fields(
-        node, node_controller.NODE_SCHEMA['properties'])
+        node, node_controller.node_schema()['properties'])
 
 
 def port_post_data(**kw):
diff --git a/releasenotes/notes/network_data_schema-9342edf3c47b2a66.yaml b/releasenotes/notes/network_data_schema-9342edf3c47b2a66.yaml
new file mode 100644
index 0000000000..803551bb41
--- /dev/null
+++ b/releasenotes/notes/network_data_schema-9342edf3c47b2a66.yaml
@@ -0,0 +1,5 @@
+---
+features:
+  - |
+    The network data schema is now configurable via the new configuration
+    options ``[api]network_data_schema``.