diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema
index 967b5f9904..f5fb611bd2 100644
--- a/data/schemas/keyboard.jsonschema
+++ b/data/schemas/keyboard.jsonschema
@@ -85,8 +85,16 @@
         "layout_aliases": {
             "type": "object",
             "additionalProperties": {
-                "type": "string",
-                "pattern": "^LAYOUT_[0-9a-z_]*$"
+                "oneOf": [
+                    {
+                        "type": "string",
+                        "enum": ["LAYOUT"]
+                    },
+                    {
+                        "type": "string",
+                        "pattern": "^LAYOUT_[0-9a-z_]*$"
+                    }
+                ]
             }
         },
         "layouts": {
diff --git a/lib/python/qmk/c_parse.py b/lib/python/qmk/c_parse.py
index 89dd278b7e..d4f39c8839 100644
--- a/lib/python/qmk/c_parse.py
+++ b/lib/python/qmk/c_parse.py
@@ -46,7 +46,7 @@ def find_layouts(file):
     parsed_layouts = {}
 
     # Search the file for LAYOUT macros and aliases
-    file_contents = file.read_text()
+    file_contents = file.read_text(encoding='utf-8')
     file_contents = comment_remover(file_contents)
     file_contents = file_contents.replace('\\\n', '')
 
@@ -87,12 +87,7 @@ def find_layouts(file):
             except ValueError:
                 continue
 
-    # Populate our aliases
-    for alias, text in aliases.items():
-        if text in parsed_layouts and 'KEYMAP' not in alias:
-            parsed_layouts[alias] = parsed_layouts[text]
-
-    return parsed_layouts
+    return parsed_layouts, aliases
 
 
 def parse_config_h_file(config_h_file, config_h=None):
@@ -104,7 +99,7 @@ def parse_config_h_file(config_h_file, config_h=None):
     config_h_file = Path(config_h_file)
 
     if config_h_file.exists():
-        config_h_text = config_h_file.read_text()
+        config_h_text = config_h_file.read_text(encoding='utf-8')
         config_h_text = config_h_text.replace('\\\n', '')
         config_h_text = strip_multiline_comment(config_h_text)
 
diff --git a/lib/python/qmk/cli/chibios/confmigrate.py b/lib/python/qmk/cli/chibios/confmigrate.py
index 3e348b2b07..89995931a4 100644
--- a/lib/python/qmk/cli/chibios/confmigrate.py
+++ b/lib/python/qmk/cli/chibios/confmigrate.py
@@ -40,7 +40,7 @@ file_header = """\
 
 
 def collect_defines(filepath):
-    with open(filepath, 'r') as f:
+    with open(filepath, 'r', encoding='utf-8') as f:
         content = f.read()
         define_search = re.compile(r'(?m)^#\s*define\s+(?:.*\\\r?\n)*.*$', re.MULTILINE)
         value_search = re.compile(r'^#\s*define\s+(?P<name>[a-zA-Z0-9_]+(\([^\)]*\))?)\s*(?P<value>.*)', re.DOTALL)
@@ -146,17 +146,17 @@ def chibios_confmigrate(cli):
         if cli.args.input.name == "chconf.h" and ("CHCONF_H" in input_defs["dict"] or "_CHCONF_H_" in input_defs["dict"] or cli.args.force):
             migrate_chconf_h(to_override, outfile=sys.stdout)
             if cli.args.overwrite:
-                with open(cli.args.input, "w") as out_file:
+                with open(cli.args.input, "w", encoding='utf-8') as out_file:
                     migrate_chconf_h(to_override, outfile=out_file)
 
         elif cli.args.input.name == "halconf.h" and ("HALCONF_H" in input_defs["dict"] or "_HALCONF_H_" in input_defs["dict"] or cli.args.force):
             migrate_halconf_h(to_override, outfile=sys.stdout)
             if cli.args.overwrite:
-                with open(cli.args.input, "w") as out_file:
+                with open(cli.args.input, "w", encoding='utf-8') as out_file:
                     migrate_halconf_h(to_override, outfile=out_file)
 
         elif cli.args.input.name == "mcuconf.h" and ("MCUCONF_H" in input_defs["dict"] or "_MCUCONF_H_" in input_defs["dict"] or cli.args.force):
             migrate_mcuconf_h(to_override, outfile=sys.stdout)
             if cli.args.overwrite:
-                with open(cli.args.input, "w") as out_file:
+                with open(cli.args.input, "w", encoding='utf-8') as out_file:
                     migrate_mcuconf_h(to_override, outfile=out_file)
diff --git a/lib/python/qmk/cli/generate/layouts.py b/lib/python/qmk/cli/generate/layouts.py
index b7baae0651..15b289522e 100755
--- a/lib/python/qmk/cli/generate/layouts.py
+++ b/lib/python/qmk/cli/generate/layouts.py
@@ -82,6 +82,10 @@ def generate_layouts(cli):
         layouts_h_lines.append(rows)
         layouts_h_lines.append('}')
 
+    for alias, target in kb_info_json.get('layout_aliases', {}).items():
+        layouts_h_lines.append('')
+        layouts_h_lines.append('#define %s %s' % (alias, target))
+
     # Show the results
     layouts_h = '\n'.join(layouts_h_lines) + '\n'
 
diff --git a/lib/python/qmk/cli/info.py b/lib/python/qmk/cli/info.py
index 87d7253d4b..a7ce8abf03 100755
--- a/lib/python/qmk/cli/info.py
+++ b/lib/python/qmk/cli/info.py
@@ -29,7 +29,7 @@ def show_keymap(kb_info_json, title_caps=True):
         else:
             cli.echo('{fg_blue}keymap_%s{fg_reset}:', cli.config.info.keymap)
 
-        keymap_data = json.load(keymap_path.open())
+        keymap_data = json.load(keymap_path.open(encoding='utf-8'))
         layout_name = keymap_data['layout']
 
         for layer_num, layer in enumerate(keymap_data['layers']):
@@ -57,7 +57,7 @@ def show_matrix(kb_info_json, title_caps=True):
         # Build our label list
         labels = []
         for key in layout['layout']:
-            if key['matrix']:
+            if 'matrix' in key:
                 row = ROW_LETTERS[key['matrix'][0]]
                 col = COL_LETTERS[key['matrix'][1]]
 
@@ -91,6 +91,9 @@ def print_friendly_output(kb_info_json):
         cli.echo('{fg_blue}Size{fg_reset}: %s x %s' % (kb_info_json['width'], kb_info_json['height']))
     cli.echo('{fg_blue}Processor{fg_reset}: %s', kb_info_json.get('processor', 'Unknown'))
     cli.echo('{fg_blue}Bootloader{fg_reset}: %s', kb_info_json.get('bootloader', 'Unknown'))
+    if 'layout_aliases' in kb_info_json:
+        aliases = [f'{key}={value}' for key, value in kb_info_json['layout_aliases'].items()]
+        cli.echo('{fg_blue}Layout aliases:{fg_reset} %s' % (', '.join(aliases),))
 
     if cli.config.info.layouts:
         show_layouts(kb_info_json, True)
diff --git a/lib/python/qmk/cli/kle2json.py b/lib/python/qmk/cli/kle2json.py
index 66d504bfc2..3bb7443582 100755
--- a/lib/python/qmk/cli/kle2json.py
+++ b/lib/python/qmk/cli/kle2json.py
@@ -27,7 +27,7 @@ def kle2json(cli):
         cli.log.error('File {fg_cyan}%s{style_reset_all} was not found.', file_path)
         return False
     out_path = file_path.parent
-    raw_code = file_path.open().read()
+    raw_code = file_path.read_text(encoding='utf-8')
     # Check if info.json exists, allow overwrite with force
     if Path(out_path, "info.json").exists() and not cli.args.force:
         cli.log.error('File {fg_cyan}%s/info.json{style_reset_all} already exists, use -f or --force to overwrite.', out_path)
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index 2accaba9e4..cf5dc6640b 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -45,7 +45,12 @@ def info_json(keyboard):
         info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'}
 
     # Populate layout data
-    for layout_name, layout_json in _find_all_layouts(info_data, keyboard).items():
+    layouts, aliases = _find_all_layouts(info_data, keyboard)
+
+    if aliases:
+        info_data['layout_aliases'] = aliases
+
+    for layout_name, layout_json in layouts.items():
         if not layout_name.startswith('LAYOUT_kc'):
             layout_json['c_macro'] = True
             info_data['layouts'][layout_name] = layout_json
@@ -92,7 +97,7 @@ def _json_load(json_file):
     Note: file must be a Path object.
     """
     try:
-        return hjson.load(json_file.open())
+        return hjson.load(json_file.open(encoding='utf-8'))
 
     except json.decoder.JSONDecodeError as e:
         cli.log.error('Invalid JSON encountered attempting to load {fg_cyan}%s{fg_reset}:\n\t{fg_red}%s', json_file, e)
@@ -415,21 +420,28 @@ def _merge_layouts(info_data, new_info_data):
 
 def _search_keyboard_h(path):
     current_path = Path('keyboards/')
+    aliases = {}
     layouts = {}
+
     for directory in path.parts:
         current_path = current_path / directory
         keyboard_h = '%s.h' % (directory,)
         keyboard_h_path = current_path / keyboard_h
         if keyboard_h_path.exists():
-            layouts.update(find_layouts(keyboard_h_path))
+            new_layouts, new_aliases = find_layouts(keyboard_h_path)
+            layouts.update(new_layouts)
 
-    return layouts
+            for alias, alias_text in new_aliases.items():
+                if alias_text in layouts:
+                    aliases[alias] = alias_text
+
+    return layouts, aliases
 
 
 def _find_all_layouts(info_data, keyboard):
     """Looks for layout macros associated with this keyboard.
     """
-    layouts = _search_keyboard_h(Path(keyboard))
+    layouts, aliases = _search_keyboard_h(Path(keyboard))
 
     if not layouts:
         # If we don't find any layouts from info.json or keyboard.h we widen our search. This is error prone which is why we want to encourage people to follow the standard above.
@@ -437,11 +449,15 @@ def _find_all_layouts(info_data, keyboard):
 
         for file in glob('keyboards/%s/*.h' % keyboard):
             if file.endswith('.h'):
-                these_layouts = find_layouts(file)
+                these_layouts, these_aliases = find_layouts(file)
+
                 if these_layouts:
                     layouts.update(these_layouts)
 
-    return layouts
+                if these_aliases:
+                    aliases.update(these_aliases)
+
+    return layouts, aliases
 
 
 def _log_error(info_data, message):
@@ -540,11 +556,19 @@ def merge_info_jsons(keyboard, info_data):
             cli.log.error('\t%s: %s', json_path, e.message)
             continue
 
-        # Mark the layouts as coming from json
-        for layout in new_info_data.get('layouts', {}).values():
-            layout['c_macro'] = False
+        # Merge layout data in
+        for layout_name, layout in new_info_data.get('layouts', {}).items():
+            if layout_name in info_data['layouts']:
+                for new_key, existing_key in zip(layout['layout'], info_data['layouts'][layout_name]['layout']):
+                    existing_key.update(new_key)
+            else:
+                layout['c_macro'] = False
+                info_data['layouts'][layout_name] = layout
 
         # Update info_data with the new data
+        if 'layouts' in new_info_data:
+            del (new_info_data['layouts'])
+
         deep_update(info_data, new_info_data)
 
     return info_data
diff --git a/lib/python/qmk/keymap.py b/lib/python/qmk/keymap.py
index 266532f503..d8495c38bc 100644
--- a/lib/python/qmk/keymap.py
+++ b/lib/python/qmk/keymap.py
@@ -42,7 +42,7 @@ def template_json(keyboard):
     template_file = Path('keyboards/%s/templates/keymap.json' % keyboard)
     template = {'keyboard': keyboard}
     if template_file.exists():
-        template.update(json.loads(template_file.read_text()))
+        template.update(json.load(template_file.open(encoding='utf-8')))
 
     return template
 
@@ -58,7 +58,7 @@ def template_c(keyboard):
     """
     template_file = Path('keyboards/%s/templates/keymap.c' % keyboard)
     if template_file.exists():
-        template = template_file.read_text()
+        template = template_file.read_text(encoding='utf-8')
     else:
         template = DEFAULT_KEYMAP_C
 
@@ -469,7 +469,7 @@ def parse_keymap_c(keymap_file, use_cpp=True):
         if use_cpp:
             keymap_file = _c_preprocess(keymap_file)
         else:
-            keymap_file = keymap_file.read_text()
+            keymap_file = keymap_file.read_text(encoding='utf-8')
 
     keymap = dict()
     keymap['layers'] = _get_layers(keymap_file)
diff --git a/lib/python/qmk/os_helpers/linux/__init__.py b/lib/python/qmk/os_helpers/linux/__init__.py
index a04ac4f8a9..9e73964e47 100644
--- a/lib/python/qmk/os_helpers/linux/__init__.py
+++ b/lib/python/qmk/os_helpers/linux/__init__.py
@@ -95,7 +95,7 @@ def check_udev_rules():
 
         # Collect all rules from the config files
         for rule_file in udev_rules:
-            for line in rule_file.read_text().split('\n'):
+            for line in rule_file.read_text(encoding='utf-8').split('\n'):
                 line = line.strip()
                 if not line.startswith("#") and len(line):
                     current_rules.add(line)
diff --git a/lib/python/qmk/tests/test_cli_commands.py b/lib/python/qmk/tests/test_cli_commands.py
index 3efeddb85e..82c42a20e8 100644
--- a/lib/python/qmk/tests/test_cli_commands.py
+++ b/lib/python/qmk/tests/test_cli_commands.py
@@ -16,7 +16,7 @@ def check_subcommand(command, *args):
 def check_subcommand_stdin(file_to_read, command, *args):
     """Pipe content of a file to a command and return output.
     """
-    with open(file_to_read) as my_file:
+    with open(file_to_read, encoding='utf-8') as my_file:
         cmd = ['bin/qmk', command, *args]
         result = run(cmd, stdin=my_file, stdout=PIPE, stderr=STDOUT, universal_newlines=True)
     return result