Repository: hed-standard/hed-python (language: python)

Issue:
Web work



Discussion / hints:

Fix patch:
diff --git a/hedtools/examples/run_hed_conversion_examples.py b/hedtools/examples/run_hed_conversion_examples.py
index 87356751a..8d4ca15f1 100644
--- a/hedtools/examples/run_hed_conversion_examples.py
+++ b/hedtools/examples/run_hed_conversion_examples.py
@@ -1,5 +1,10 @@
 """
-Examples of opening a HED dataset spreadsheet and resaving the entire thing with tags converted to short or long.
+Examples of opening a HED dataset spreadsheet, processing it, then saving it back out as a spreadsheet.
+This example is specifically converting tags between short and long form.
+
+Classes Demonstrated:
+HedFileInput - Used to open/modify/save a spreadsheet
+TagFormat - Used to convert between short and long tags
 """
 import os
 
diff --git a/hedtools/examples/run_hed_def_mapper_example.py b/hedtools/examples/run_hed_def_mapper_example.py
index 8a498453e..e0ca47601 100644
--- a/hedtools/examples/run_hed_def_mapper_example.py
+++ b/hedtools/examples/run_hed_def_mapper_example.py
@@ -1,31 +1,48 @@
 """
-Example of outputting a spreadsheet after processing is done(replacing definitions etc)
+Examples of creating an EventFileInput to open a spreadsheet, process it, then save it back out.
+
+Classes Demonstrated:
+HedSchema - Opens a hed xml schema.  Used by other tools to check tag attributes in the schema.
+HedValidator - Validates a given input string or file
+EventFileInput - Used to open/modify/save a bids style spreadsheet, with json sidecars and definitions.
+HedFileError - Exception thrown when a file cannot be opened.(parsing error, file not found error, etc)
+ColumnDefGroup - Contains the data from a single json sidecar, can be validated using a HedSchema.
+DefDict - Created from a ColumnDefGroup.  Contains all label definitions to be expanded, can be validated using
+          a HedSchema.
 """
 import os
 from hed.util.event_file_input import EventFileInput
 from hed.util.hed_schema import HedSchema
 from hed.validator.hed_validator import HedValidator
+from hed.util.column_def_group import ColumnDefGroup
 
 if __name__ == '__main__':
     local_hed_file = 'data/HED8.0.0-alpha.1.xml'
-    example_data_path = 'data'   # path to example data
+    example_data_path = 'data'  # path to example data
     hed3_tags_single_sheet = os.path.join(example_data_path, 'hed_tag_def_example.xlsx')
 
-    hed_dict = HedSchema(local_hed_file)
+    hed_schema = HedSchema(local_hed_file)
     prefixed_needed_tag_columns = {2: 'Event/Label/', 3: 'Event/Description/'}
-    input_file = EventFileInput(hed3_tags_single_sheet, json_def_files="data/both_types_events_def_example.json",
+    json_file = "data/both_types_events_def_example.json"
+    column_group = ColumnDefGroup(json_file)
+    def_dict, def_issues = column_group.extract_defs(hed_schema)
+    if def_issues:
+        print(HedValidator.get_printable_issue_string(def_issues,
+                                                      title="There should be no errors in the definitions from the sidecars:"))
+    input_file = EventFileInput(hed3_tags_single_sheet, json_def_files=column_group,
                                 tag_columns=[4], column_prefix_dictionary=prefixed_needed_tag_columns,
                                 worksheet_name='LKT Events',
-                                hed_schema=hed_dict)
+                                hed_schema=hed_schema,
+                                def_dicts=def_dict)
 
-    validation_issues = input_file.validate_file_sidecars(hed_schema=hed_dict)
+    validation_issues = input_file.validate_file_sidecars(hed_schema=hed_schema)
     if validation_issues:
-        print("There should be no errors with the sidecar.  This will likely cause other errors.")
-        for issue in validation_issues:
-            print(f"{issue['code']}: {issue['message']}")
-    validator = HedValidator(input_file, hed_schema=hed_dict)
-    print(validator.get_printable_issue_string(
-        "Normal hed string errors"))
+        print(HedValidator.get_printable_issue_string(validation_issues,
+                                                      title="There should be no errors with the sidecar.  This will likely cause other errors if there are."))
+    validator = HedValidator(hed_schema=hed_schema)
+    validation_issues = validator.validate_input(input_file)
+    print(validator.get_printable_issue_string(validation_issues,
+                                               "Normal hed string errors"))
 
     input_file.save(include_formatting=True, add_suffix="_test_output", output_processed_file=False)
     input_file.save(include_formatting=True, add_suffix="_proc_test_output", output_processed_file=True)
diff --git a/hedtools/examples/run_hed_files_examples.py b/hedtools/examples/run_hed_files_examples.py
index c3c2f48af..b110181f5 100644
--- a/hedtools/examples/run_hed_files_examples.py
+++ b/hedtools/examples/run_hed_files_examples.py
@@ -1,114 +1,115 @@
 """
-Examples of calling the HED validator with files of tags
-"""
+Examples of creating a HedValidator and validating various spreadsheets using it.
+Also contains examples of catching HedFileErrors for invalid input.
 
+Classes Demonstrated:
+HedFileInput - Used to open/modify/save a spreadsheet
+HedValidator - Validates a given input string or file
+HedFileError - Exception thrown when a file cannot be opened.(parsing error, file not found error, etc)
+"""
 
 import os
 
 from hed.validator.hed_validator import HedValidator
 from hed.util.hed_file_input import HedFileInput
+from hed.util.exceptions import HedFileError
 
 if __name__ == '__main__':
     # Set up the file names for the tests
-    local_hed_file = 'data/HED7.1.1.xml'   # path HED v7.1.1 stored locally
-    example_data_path = 'data'   # path to example data
+    local_hed_file = 'data/HED7.1.1.xml'  # path HED v7.1.1 stored locally
+    example_data_path = 'data'  # path to example data
     valid_tsv_file = os.path.join(example_data_path, 'ValidTwoColumnHED7_1_1.tsv')
     valid_tsv_file_no_header = os.path.join(example_data_path, 'ValidTwoColumnHED7_1_1NoHeader.tsv')
     valid_tsv_file_separate_cols = os.path.join(example_data_path, 'ValidSeparateColumnTSV.txt')
     unsupported_csv_format = os.path.join(example_data_path, 'UnsupportedFormatCSV.csv')
     multiple_sheet_xlsx_file = os.path.join(example_data_path, 'ExcelMultipleSheets.xlsx')
 
+    hed_validator_old = HedValidator(xml_version_number='7.1.1')
+    hed_validator_local = HedValidator(hed_xml_file=local_hed_file)
+    hed_validator_local_warnings = HedValidator(hed_xml_file=local_hed_file, check_for_warnings=True
+                                                )
     # Example 1a: Valid TSV file with default version of HED
     print(valid_tsv_file)
     input_file = HedFileInput(valid_tsv_file, tag_columns=[2])
-    hed_input_reader = HedValidator(input_file, xml_version_number='7.1.1')
-    issues = hed_input_reader.get_validation_issues()
-    print(hed_input_reader.get_printable_issue_string(
-        '[Example 1a] ValidTwoColumnHED7_1_1 is probably okay with default version of HED'))
+    validation_issues = hed_validator_old.validate_input(input_file)
+    print(HedValidator.get_printable_issue_string(validation_issues, title='[Example 1a] ValidTwoColumnHED7_1_1 is probably okay with default version of HED'))
 
     # Example 1b: Valid TSV file with specified local version of HED
     print(valid_tsv_file)
     input_file = HedFileInput(valid_tsv_file, tag_columns=[2])
-    hed_input_reader = HedValidator(input_file, hed_xml_file=local_hed_file)
-    print(hed_input_reader.get_printable_issue_string(
-        '[Example 1b] ValidTwoColumnHED7_1_1 should have no issues with local version 7.1.1'))
+    validation_issues = hed_validator_local.validate_input(input_file)
+    print(HedValidator.get_printable_issue_string(validation_issues, title='[Example 1b] ValidTwoColumnHED7_1_1 should have no issues with local version 7.1.1'))
 
     # Example 1c: Valid TSV file with specified local version of HED and no column headers
     print(valid_tsv_file_no_header)
     input_file = HedFileInput(valid_tsv_file_no_header, has_column_names=False, tag_columns=[2, 3])
-    hed_input_reader = HedValidator(input_file, hed_xml_file=local_hed_file)
-    print(hed_input_reader.get_printable_issue_string(
-        '[Example 1c] ValidTwoColumnHED7_1_1NoHeaders should have no issues with version 7.1.1'))
+    validation_issues = hed_validator_local.validate_input(input_file)
+    print(HedValidator.get_printable_issue_string(validation_issues, title='[Example 1c] ValidTwoColumnHED7_1_1NoHeaders should have no issues with version 7.1.1'))
 
     # Example 1d: Valid TSV with separate columns for required fields
     print(valid_tsv_file_separate_cols)
     prefixed_needed_tag_columns = {3: 'Event/Description/', 4: 'Event/Label/', 5: 'Event/Category/'}
     input_file = HedFileInput(valid_tsv_file_separate_cols, tag_columns=[6],
-                                      column_prefix_dictionary=prefixed_needed_tag_columns)
-    hed_input_reader = HedValidator(input_file, hed_xml_file=local_hed_file)
-    print(hed_input_reader.get_printable_issue_string(
-        '[Example 1d] Valid TSV with required tags in separate columns'))
+                              column_prefix_dictionary=prefixed_needed_tag_columns)
+    validation_issues = hed_validator_local.validate_input(input_file)
+    print(HedValidator.get_printable_issue_string(validation_issues, title='[Example 1d] Valid TSV with required tags in separate columns'))
 
     # Example 2a: CSV files are not supported (Use excel to convert)
     print(unsupported_csv_format)
     prefixed_needed_tag_columns = {3: 'Event/Description/', 4: 'Event/Label/', 5: 'Event/Category/'}
-    input_file = HedFileInput(unsupported_csv_format, tag_columns=[6],
-                                      column_prefix_dictionary=prefixed_needed_tag_columns)
-    hed_input_reader = HedValidator(input_file, hed_xml_file=local_hed_file)
-    print(hed_input_reader.get_printable_issue_string(
-        '[Example 2a] csv is unsupported format, but this call treats file name as HED string'))
+    try:
+        input_file = HedFileInput(unsupported_csv_format, tag_columns=[6],
+                                  column_prefix_dictionary=prefixed_needed_tag_columns)
+    except HedFileError as e:
+        print('[Example 2a] csv is unsupported format, but this call treats file name as HED string')
+        print(e.format_error_message(return_string_only=True))
 
     # Example 2b: CSV files are not supported (Use excel to convert) - explicit file specification
     print(unsupported_csv_format)
     prefixed_needed_tag_columns = {3: 'Event/Description/', 4: 'Event/Label/', 5: 'Event/Category/'}
-    input_file = HedFileInput(unsupported_csv_format, tag_columns=[6],
-                              column_prefix_dictionary=prefixed_needed_tag_columns)
-    hed_input_reader = HedValidator(input_file, hed_xml_file=local_hed_file)
-    print(hed_input_reader.get_printable_issue_string(
-        '[Example 2b] csv is unsupported format, now have right error message'))
+    try:
+        input_file = HedFileInput(unsupported_csv_format, tag_columns=[6],
+                                  column_prefix_dictionary=prefixed_needed_tag_columns)
+    except HedFileError as e:
+        print("['Example 2b] csv is unsupported format, now have right error message")
+        print(e.format_error_message(return_string_only=True))
 
     prefixed_needed_tag_columns = {2: 'Event/Label/', 3: 'Event/Description/'}
     # Example 3a: XLSX file with multiple sheets - first sheet has no issues with 7.1.1
     input_file = HedFileInput(multiple_sheet_xlsx_file, tag_columns=[4],
                               column_prefix_dictionary=prefixed_needed_tag_columns,
                               worksheet_name='LKT Events')
-    hed_input_reader = HedValidator(input_file, hed_xml_file=local_hed_file)
-    print(hed_input_reader.get_printable_issue_string(
-        '[Example 3a] Multiple sheet xlsl has LKT Events sheet with no issues'))
+    validation_issues = hed_validator_local.validate_input(input_file)
+    print(HedValidator.get_printable_issue_string(validation_issues, title='[Example 3a] Multiple sheet xlsl has LKT Events sheet with no issues'))
 
     # Example 3b: Valid XLSX file with multiple sheets - first sheet probably has no issues with default schema
     input_file = HedFileInput(multiple_sheet_xlsx_file, tag_columns=[4],
                               column_prefix_dictionary=prefixed_needed_tag_columns,
                               worksheet_name='LKT Events')
-    hed_input_reader = HedValidator(input_file, check_for_warnings=True, hed_xml_file=local_hed_file)
-    print(hed_input_reader.get_printable_issue_string(
-        '[Example 3b] LKT Events sheet probably has with no issues with the default schema'))
+    validation_issues = hed_validator_local_warnings.validate_input(input_file)
+    print(HedValidator.get_printable_issue_string(validation_issues, title='[Example 3b] LKT Events sheet probably has with no issues with the default schema'))
 
     # Example 3c: XLSX file with multiple sheets - assumes first sheet by default
     input_file = HedFileInput(multiple_sheet_xlsx_file, tag_columns=[4],
                               column_prefix_dictionary=prefixed_needed_tag_columns)
-    hed_input_reader = HedValidator(input_file, hed_xml_file=local_hed_file)
-    print(hed_input_reader.get_printable_issue_string(
-        '[Example 3c] Multiple sheet xlsl has first sheet with no issues'))
+    validation_issues = hed_validator_local.validate_input(input_file)
+    print(HedValidator.get_printable_issue_string(validation_issues, title='[Example 3c] Multiple sheet xlsl has first sheet with no issues'))
 
     # Example 3d: XLSX file with multiple sheets - PVT sheet has several issues with 7.1.1
     input_file = HedFileInput(multiple_sheet_xlsx_file, tag_columns=[4],
-                                      column_prefix_dictionary=prefixed_needed_tag_columns,
-                                      worksheet_name='PVT Events')
-    hed_input_reader = HedValidator(input_file, hed_xml_file=local_hed_file)
-    print(hed_input_reader.get_printable_issue_string(
-        '[Example 3d] Some PVT worksheet issues that are due to removal of extensionAllowed in places in 7.1.1'))
+                              column_prefix_dictionary=prefixed_needed_tag_columns,
+                              worksheet_name='PVT Events')
+    validation_issues = hed_validator_local.validate_input(input_file)
+    print(HedValidator.get_printable_issue_string(validation_issues, title='[Example 3d] Some PVT worksheet issues that are due to removal of extensionAllowed in places in 7.1.1'))
 
     # Example 3e: Invalid XLSX sheet with 7.1.1
     input_file = HedFileInput(multiple_sheet_xlsx_file, tag_columns=[4], worksheet_name='DAS Events')
-    hed_input_reader = HedValidator(input_file, hed_xml_file=local_hed_file)
-    print(hed_input_reader.get_printable_issue_string(
-        '[Example 3e] DAS Events worksheet has multiple issues with 7.1.1'))
+    validation_issues = hed_validator_local.validate_input(input_file)
+    print(HedValidator.get_printable_issue_string(validation_issues, title='[Example 3e] DAS Events worksheet has multiple issues with 7.1.1'))
 
     # Example 3f: Invalid XLSX sheet with 7.1.1 - also can't duplicate Label and Description in 7.1.1
     input_file = HedFileInput(multiple_sheet_xlsx_file, tag_columns=[4],
-                                      column_prefix_dictionary=prefixed_needed_tag_columns,
-                                      worksheet_name='DAS Events')
-    hed_input_reader = HedValidator(input_file, hed_xml_file=local_hed_file)
-    print(hed_input_reader.get_printable_issue_string(
-        '[Example 3f] DAS worse now because of duplicate Label and Description specifications'))
+                              column_prefix_dictionary=prefixed_needed_tag_columns,
+                              worksheet_name='DAS Events')
+    validation_issues = hed_validator_local.validate_input(input_file)
+    print(HedValidator.get_printable_issue_string(validation_issues, title='[Example 3f] DAS worse now because of duplicate Label and Description specifications'))
diff --git a/hedtools/examples/run_hed_string_examples.py b/hedtools/examples/run_hed_string_examples.py
index 01f9110a5..73b3b8760 100644
--- a/hedtools/examples/run_hed_string_examples.py
+++ b/hedtools/examples/run_hed_string_examples.py
@@ -1,36 +1,40 @@
 """
-Examples of HED validation of a tag string
-"""
+Examples of creating a HedValidator and validating various Hed Strings with it.
 
+Classes Demonstrated:
+HedValidator - Validates a given input string or file
+"""
 
 from hed.validator.hed_validator import HedValidator
 
 if __name__ == '__main__':
     local_hed_file = 'data/HED7.1.1.xml'   # path HED v7.1.1 stored locally
 
+    hed_validator_old = HedValidator(hed_xml_file=local_hed_file)
+    hed_validator_current = HedValidator()
+    hed_validator_no_semantic = HedValidator(run_semantic_validation=False)
+
     # Example 1a: Valid HED string for HED <= v7.1.1
     hed_string_1 = 'Event/Label/ButtonPuskDeny, Event/Description/Button push to deny access to the ID holder,' \
                    'Event/Category/Participant response, ' \
                    '(Participant ~ Action/Button press/Keyboard ~ Participant/Effect/Body part/Arm/Hand/Finger)'
-    hed_input_reader = HedValidator(hed_string_1, hed_xml_file=local_hed_file)
-    print(hed_input_reader.get_printable_issue_string(
-        '[Example 1a] hed_string_1 should have no issues with HEDv7.1.1'))
+    validation_issues = hed_validator_old.validate_input(hed_string_1)
+    print(HedValidator.get_printable_issue_string(validation_issues, title='[Example 1a] hed_string_1 should have no issues with HEDv7.1.1'))
 
     # Example 1b: Try with the latest version of HED.xml
-    hed_input_reader = HedValidator(hed_string_1)
-    print(hed_input_reader.get_printable_issue_string(
-        '[Example 1b] hed_string_1 has issues with the latest HED version'))
+    validation_issues = hed_validator_current.validate_input(hed_string_1)
+    print(HedValidator.get_printable_issue_string(validation_issues, title='[Example 1b] hed_string_1 has issues with the latest HED version'))
 
     # Example 2a: Invalid HED string (junk in last tag)
     hed_string_2 = 'Event/Category/Participant response,'  \
                    '(Participant ~ Action/Button press/Keyboard ~ Participant/Effect/Body part/Arm/Hand/Finger),' \
                    'dskfjkf/dskjdfkj/sdkjdsfkjdf/sdlfdjdsjklj'
-    hed_input_reader = HedValidator(hed_string_2, hed_xml_file=local_hed_file)
-    print(hed_input_reader.get_printable_issue_string('[Example 2a] hed_string_2 has junk in the last tag'))
+    validation_issues = hed_validator_old.validate_input(hed_string_2)
+    print(HedValidator.get_printable_issue_string(validation_issues, title='[Example 2a] hed_string_2 has junk in the last tag'))
 
     # Example 2b: However HED string of Example 2 has valid syntax so syntactic validation works
-    hed_input_reader = HedValidator(hed_string_2, run_semantic_validation=False)
-    print(hed_input_reader.get_printable_issue_string('[Example 2b] hed_string_2 is syntactically correct'))
+    validation_issues = hed_validator_no_semantic.validate_input(hed_string_2)
+    print(HedValidator.get_printable_issue_string(validation_issues, title='[Example 2b] hed_string_2 is syntactically correct'))
 
     # Example 3a: Invalid HED string
     hed_string_3 = 'Event/Description/The start of increasing the size of sector, Event/Label/Sector start, ' \
@@ -38,19 +42,16 @@
                    '(Item/2D shape/Sector, Attribute/Visual/Color/Red) ' \
                    '(Item/2D shape/Ellipse/Circle, Attribute/Visual/Color / Red), Sensory presentation/Visual, ' \
                    'Participant/Efffectt/Visual, Participant/Effect/Cognitive/Target'
-    hed_input_reader = HedValidator(hed_string_3)
-    print(hed_input_reader.get_printable_issue_string(
-        '[Example 3a] hed_string_3 has missing comma so fails before Efffectt typo'))
+    validation_issues = hed_validator_current.validate_input(hed_string_3)
+    print(HedValidator.get_printable_issue_string(validation_issues, title='[Example 3a] hed_string_3 has missing comma so fails before Efffectt typo'))
 
     # Example 3b: Using issue list directly - issues are returned as a list of dictionaries
-    issues = hed_input_reader.get_validation_issues()
-    print('[Example 3b] hed_string_3 has ', len(issues), ' issues\n')
+    # print('[Example 3b] hed_string_3 has ', len(validation_issues), ' issues\n')
 
     # Example 4a: Example using the ~ notation
     hed_string_4 = 'Event/Label/ButtonDeny, Event/Description/Button push to deny access to the ID holder, ' \
                    'Event/Category/Participant response, ' \
                    '(Participant ~ Action/Button press/Keyboard ~ Participant/Effect/Body part/Arm/Hand/Finger), ' \
                    '(Participant ~ Action/Deny/Access ~ Item/Object/Person/IDHolder)'
-    hed_input_reader = HedValidator(hed_string_4, hed_xml_file=local_hed_file)
-    print(hed_input_reader.get_printable_issue_string(
-        '[Example 4a] the ~ notation in hed_string_4 works in v7.1.1'))
+    validation_issues = hed_validator_current.validate_input(hed_string_4)
+    print(HedValidator.get_printable_issue_string(validation_issues, title='[Example 4a] the ~ notation in hed_string_4 works in v7.1.1'))
diff --git a/hedtools/examples/run_json_validation_and_conversion.py b/hedtools/examples/run_json_validation_and_conversion.py
index 6abee0e1e..e8e13f8b0 100644
--- a/hedtools/examples/run_json_validation_and_conversion.py
+++ b/hedtools/examples/run_json_validation_and_conversion.py
@@ -1,6 +1,15 @@
-"""Examples of how to validate errors in a json sidecar file independently, and also an example of
-        opening a json file and converting each HED tag from short to long."""
+"""
+Example 1: creating a ColumnDefGroup from a json sidecar file and validating it
 
+Example 2: how to open a json sidecar, modify it, then save it back out.
+
+Example 3: Iterate over hed strings in a json sidecar.
+
+Classes Demonstrated:
+HedSchema - Opens a hed xml schema.  Used by other tools to check tag attributes in the schema.
+TagFormat - Used to convert between short and long tags
+ColumnDefGroup - Contains the data from a single json sidecar, can be validated using a HedSchema.
+"""
 from hed.util.column_def_group import ColumnDefGroup
 from hed.util.hed_schema import HedSchema
 from hed.tools.tag_format import TagFormat
@@ -9,13 +18,13 @@
 hed_schema = HedSchema(local_hed_xml)
 json_filename = "data/both_types_events_errors.json"
 
+# Example 1
 json_file = ColumnDefGroup(json_filename)
 # Print all the errors from the json file
 errors = json_file.validate_entries(hed_schema)
-for error in errors.values():
-    for sub_error in error:
-        print(sub_error["message"])
+print(json_file.get_printable_issue_string(errors))
 
+# Example 2
 # Open the json file, convert all tags to long, and save it out
 long_tag_formatter = TagFormat(local_hed_xml)
 for column_def in json_file:
@@ -27,6 +36,7 @@
 # Save off a copy of the input json, modified.
 json_file.save_as_json(json_filename + "-long.json")
 
+# Example 3
 # Alternate shorter syntax for accessing HED strings in a json file.
 for hed_string, position in json_file.hed_string_iter(include_position=True):
     print(hed_string)
diff --git a/hedtools/examples/run_validate_schema.py b/hedtools/examples/run_validate_schema.py
index f492baaed..f072030eb 100644
--- a/hedtools/examples/run_validate_schema.py
+++ b/hedtools/examples/run_validate_schema.py
@@ -1,16 +1,18 @@
-# Example of validating a given schema XML file.
+"""
+Example of how to validate a given hed schema file.
 
+Functions Demonstrated:
+schema_validator.validate_schema - Takes a schema XML filename as input and returns all validation issues found within.
+"""
 from hed.schema import schema_validator
 
 if __name__ == '__main__':
     # This old schema should produce many issues, including many duplicate terms
     local_hed_xml = "data/HED7.1.1.xml"
     issues = schema_validator.validate_schema(local_hed_xml)
-    for issue in issues:
-        print(issue['message'])
+    print(schema_validator.get_printable_issue_string(issues))
 
     # this should produce fairly minimal issues.
     local_hed_xml = "data/HED8.0.0-alpha.1.xml"
     issues = schema_validator.validate_schema(local_hed_xml)
-    for issue in issues:
-        print(issue['message'])
+    print(schema_validator.get_printable_issue_string(issues))
diff --git a/hedtools/examples/run_wiki2xml.py b/hedtools/examples/run_wiki2xml.py
index 549efc418..e1cb57262 100644
--- a/hedtools/examples/run_wiki2xml.py
+++ b/hedtools/examples/run_wiki2xml.py
@@ -1,12 +1,19 @@
-"""Example for how to convert from mediawiki format to xml format"""
+"""
+Example of converting a hed schema from .mediawiki format to .xml format.
+
+Functions Demonstrated:
+wiki2xml.convert_hed_wiki_2_xml - Converts hed schema from given url or filename,
+                                    returns a temporary filename for the converted file and a list of issues.
+ErrorHandler.get_printable_issue_string - Static method that converts the list of issues to a human readable form.
+"""
 from shutil import move
 from hed.schema import wiki2xml
+from hed.util.error_reporter import ErrorHandler
 
 if __name__ == '__main__':
     hed_wiki_url = 'https://raw.githubusercontent.com/hed-standard/hed-specification/master/HED-generation3-schema.mediawiki'
     local_hed_file = None
     xml_location, errors = wiki2xml.convert_hed_wiki_2_xml(hed_wiki_url, local_wiki_file=local_hed_file)
-    for issue in errors:
-        print(issue['message'])
+    ErrorHandler.get_printable_issue_string(validation_issues=errors, title="Errors in HED-generation3-schema.mediawiki")
     if xml_location:
         move(xml_location, "output.xml")
\ No newline at end of file
diff --git a/hedtools/examples/run_xml2wiki.py b/hedtools/examples/run_xml2wiki.py
index fa28adbed..637d161cb 100644
--- a/hedtools/examples/run_xml2wiki.py
+++ b/hedtools/examples/run_xml2wiki.py
@@ -1,12 +1,20 @@
-"""Example for how to convert from xml format to mediawiki format"""
+"""
+Example of converting a hed schema from .xml format to .mediawiki format.
+
+Functions Demonstrated:
+wiki2xml.convert_hed_xml_2_wiki - Converts hed schema from given url or filename,
+                                    returns a temporary filename for the converted file and a list of issues.
+ErrorHandler.get_printable_issue_string - Static method that converts the list of issues to a human readable form.
+"""
+
 from shutil import move
 from hed.schema import xml2wiki
+from hed.util.error_reporter import ErrorHandler
 
 if __name__ == '__main__':
     hed_xml_url = 'https://raw.githubusercontent.com/hed-standard/hed-specification/master/hedxml/HED7.1.1.xml'
     local_xml_file = None
     mediawiki_location, errors = xml2wiki.convert_hed_xml_2_wiki(hed_xml_url, local_xml_file)
-    for issue in errors:
-        print(issue['message'])
+    ErrorHandler.get_printable_issue_string(validation_issues=errors, title="Errors in HED7.1.1.xml")
     if mediawiki_location:
         move(mediawiki_location, "HED7.1.1.mediawiki")
diff --git a/hedtools/hed/schema/parsewiki.py b/hedtools/hed/schema/parsewiki.py
index c500eb4c5..d8546a6f4 100644
--- a/hedtools/hed/schema/parsewiki.py
+++ b/hedtools/hed/schema/parsewiki.py
@@ -4,8 +4,7 @@
 
 from xml.etree.ElementTree import Element, SubElement
 from hed.schema import parsetag, constants
-from hed.util.exceptions import SchemaFileError
-from hed.util.error_types import SchemaErrors
+from hed.util.exceptions import HedFileError, HedExceptions
 
 import os
 
@@ -133,7 +132,6 @@ def hed_wiki_2_xml_tree(wiki_file_path):
     ----------
     wiki_file_path: str
         The location of the HED wiki file.
-
     Returns
     -------
     Element
@@ -156,7 +154,7 @@ def hed_wiki_2_xml_tree(wiki_file_path):
                     break
                 line = wiki_file.readline()
     except FileNotFoundError as e:
-        raise SchemaFileError(SchemaErrors.FILE_NOT_FOUND, e.strerror, wiki_file_path)
+        raise HedFileError(HedExceptions.FILE_NOT_FOUND, e.strerror, wiki_file_path)
     return hed_node
 
 
diff --git a/hedtools/hed/schema/schema_validator.py b/hedtools/hed/schema/schema_validator.py
index 51f34f4b4..7639d9da9 100644
--- a/hedtools/hed/schema/schema_validator.py
+++ b/hedtools/hed/schema/schema_validator.py
@@ -1,13 +1,13 @@
 from hed.util.hed_schema import HedSchema
-from hed.util.error_reporter import format_schema_error, format_schema_warning, push_error_context, pop_error_context
+from hed.util import error_reporter
 from hed.util.error_types import SchemaErrors, SchemaWarnings, ErrorContext
-from hed.util.exceptions import SchemaFileError
 
 ALLOWED_TAG_CHARS = "-"
 ALLOWED_DESC_CHARS = "-_:;,./()+ ^"
 
 
-def validate_schema(hed_xml_file, also_check_for_warnings=True):
+def validate_schema(hed_xml_file, also_check_for_warnings=True, display_filename=None,
+                    error_handler=None):
     """
         Does validation of schema and returns a list of errors and warnings.
 
@@ -17,37 +17,45 @@ def validate_schema(hed_xml_file, also_check_for_warnings=True):
         filepath to a HED XML file to validate
     also_check_for_warnings : bool, default True
         If True, also checks for formatting issues like invalid characters, capitalization, etc.
+    display_filename: str
+        If present, will use this as the filename for context, rather than using the actual filename
+        Useful for temp filenames.
+    error_handler : ErrorHandler or None
+        Used to report errors.  Uses a default one if none passed in.
     Returns
     -------
     issue_list : [{}]
         A list of all warnings and errors found in the file.
     """
+    if error_handler is None:
+        error_handler = error_reporter.ErrorHandler()
     issues_list = []
-    try:
-        hed_dict = HedSchema(hed_xml_file)
-    except SchemaFileError as e:
-        return e.format_error_message()
+    hed_dict = HedSchema(hed_xml_file)
+
+    if not display_filename:
+        display_filename = hed_xml_file
+    error_handler.push_error_context(ErrorContext.FILE_NAME, display_filename)
 
-    push_error_context(ErrorContext.FILE_NAME, hed_xml_file)
     if hed_dict.has_duplicate_tags():
         duplicate_dict = hed_dict.find_duplicate_tags()
         for tag_name, long_org_tags in duplicate_dict.items():
-            issues_list += format_schema_error(SchemaErrors.DUPLICATE_TERMS, tag_name, duplicate_tag_list=long_org_tags)
+            issues_list += error_handler.format_schema_error(SchemaErrors.DUPLICATE_TERMS, tag_name,
+                                                             duplicate_tag_list=long_org_tags)
 
     if also_check_for_warnings:
         hed_terms = hed_dict.get_all_terms()
         for hed_term in hed_terms:
-            issues_list += validate_schema_term(hed_term)
+            issues_list += validate_schema_term(hed_term, error_handler=error_handler)
 
         tag_descs = hed_dict.get_all_descriptions()
         for tag_name, desc in tag_descs.items():
-            issues_list += validate_schema_description(tag_name, desc)
+            issues_list += validate_schema_description(tag_name, desc, error_handler=error_handler)
 
-    pop_error_context()
+    error_handler.pop_error_context()
     return issues_list
 
 
-def validate_schema_term(hed_term):
+def validate_schema_term(hed_term, error_handler):
     """
     Takes a single term(ie short tag) and checks capitalization and illegal characters.
 
@@ -55,6 +63,8 @@ def validate_schema_term(hed_term):
     ----------
     hed_term : str
         A single hed term
+    error_handler : ErrorHandler
+        Used to report errors
     Returns
     -------
     issue_list: [{}]
@@ -66,19 +76,19 @@ def validate_schema_term(hed_term):
 
     for i, char in enumerate(hed_term):
         if i == 0 and not (char.isdigit() or char.isupper()):
-            issues_list += format_schema_warning(SchemaWarnings.INVALID_CAPITALIZATION, hed_term,
-                                                 error_index=i, problem_char=char)
+            issues_list += error_handler.format_schema_warning(SchemaWarnings.INVALID_CAPITALIZATION, hed_term,
+                                                               error_index=i, problem_char=char)
             continue
         if char in ALLOWED_TAG_CHARS:
             continue
         if char.isalnum():
             continue
-        issues_list += format_schema_warning(SchemaWarnings.INVALID_CHARACTERS_IN_TAG, hed_term,
-                                             error_index=i, problem_char=char)
+        issues_list += error_handler.format_schema_warning(SchemaWarnings.INVALID_CHARACTERS_IN_TAG, hed_term,
+                                                           error_index=i, problem_char=char)
     return issues_list
 
 
-def validate_schema_description(tag_name, hed_description):
+def validate_schema_description(tag_name, hed_description, error_handler):
     """
     Takes a single term description and returns a list of warnings and errors in it.
 
@@ -88,6 +98,8 @@ def validate_schema_description(tag_name, hed_description):
         A single hed tag - not validated here, just used for error messages
     hed_description: str
         The description string to validate
+    error_handler : ErrorHandler
+        Used to report errors
     Returns
     -------
     issue_list: [{}]
@@ -102,5 +114,28 @@ def validate_schema_description(tag_name, hed_description):
             continue
         if char in ALLOWED_DESC_CHARS:
             continue
-        issues_list += format_schema_warning(SchemaWarnings.INVALID_CHARACTERS_IN_DESC, tag_name, hed_description, i, char)
-    return issues_list
\ No newline at end of file
+        issues_list += error_handler.format_schema_warning(SchemaWarnings.INVALID_CHARACTERS_IN_DESC, tag_name,
+                                                           hed_description, i, char)
+    return issues_list
+
+
+def get_printable_issue_string(validation_issues, title=None, severity=None, skip_filename=True):
+    """Return a string with issues list flatted into single string, one per line
+
+    Parameters
+    ----------
+    validation_issues: []
+        Issues to print
+    title: str
+        Optional title that will always show up first if present(even if there are no validation issues)
+    severity: int
+        Return only warnings >= severity
+    skip_filename: bool
+        If true, don't add the filename context to the printable string.
+    Returns
+    -------
+    str
+        A str containing printable version of the issues or '[]'.
+
+    """
+    return error_reporter.ErrorHandler.get_printable_issue_string(validation_issues, title, severity, skip_filename)
diff --git a/hedtools/hed/schema/wiki2xml.py b/hedtools/hed/schema/wiki2xml.py
index 2cd232f69..eae8cad12 100644
--- a/hedtools/hed/schema/wiki2xml.py
+++ b/hedtools/hed/schema/wiki2xml.py
@@ -4,10 +4,10 @@
 from hed.util import file_util
 from hed.schema import parsewiki
 from hed.schema.schema_validator import validate_schema
-from hed.util.exceptions import SchemaFileError
 
 
-def convert_hed_wiki_2_xml(hed_wiki_url, local_wiki_file=None, check_for_issues=True):
+def convert_hed_wiki_2_xml(hed_wiki_url, local_wiki_file=None, check_for_issues=True,
+                           display_filename=None):
     """Converts the HED wiki into a XML file.
 
     Parameters
@@ -18,6 +18,9 @@ def convert_hed_wiki_2_xml(hed_wiki_url, local_wiki_file=None, check_for_issues=
         local wiki file to use(overrides hed_wiki_url)
     check_for_issues : bool
         After conversion checks for warnings like capitalization or invalid characters.
+    display_filename: str
+        If present, will use this as the filename for context, rather than using the actual filename
+        Useful for temp filenames.
     Returns
     -------
     xml_filename: str
@@ -28,14 +31,11 @@ def convert_hed_wiki_2_xml(hed_wiki_url, local_wiki_file=None, check_for_issues=
     if local_wiki_file is None:
         local_wiki_file = file_util.url_to_file(hed_wiki_url)
 
-    try:
-        hed_xml_file_location = _create_hed_xml_file(local_wiki_file)
-    except SchemaFileError as e:
-        return None, e.format_error_message()
+    hed_xml_file_location = _create_hed_xml_file(local_wiki_file)
 
     issue_list = []
     if check_for_issues:
-        warnings = validate_schema(hed_xml_file_location)
+        warnings = validate_schema(hed_xml_file_location, display_filename=display_filename)
         issue_list += warnings
 
     return hed_xml_file_location, issue_list
diff --git a/hedtools/hed/schema/xml2wiki.py b/hedtools/hed/schema/xml2wiki.py
index fa7db4a4e..18fee431e 100644
--- a/hedtools/hed/schema/xml2wiki.py
+++ b/hedtools/hed/schema/xml2wiki.py
@@ -3,9 +3,9 @@
 from hed.schema import constants
 from hed.util import file_util
 from hed.util.hed_schema import HedSchema
-from hed.util.exceptions import SchemaFileError
 from hed.schema.schema_validator import validate_schema
 
+
 class MainParseMode(Enum):
     MainTags = 1
     UnitClassTags = 2
@@ -185,7 +185,8 @@ def get_attribs_from_root_hed_node(elem):
         return final_attrib_string
 
 
-def convert_hed_xml_2_wiki(hed_xml_url, local_xml_file=None, check_for_issues=True):
+def convert_hed_xml_2_wiki(hed_xml_url, local_xml_file=None, check_for_issues=True,
+                           display_filename=None):
     """Converts the local HED xml file into a wikimedia file
 
     Parameters
@@ -196,6 +197,9 @@ def convert_hed_xml_2_wiki(hed_xml_url, local_xml_file=None, check_for_issues=Tr
         filepath to local xml hed schema(overrides hed_xml_url)
     check_for_issues : bool
         After conversion checks for warnings like capitalization or invalid characters.
+    display_filename: str
+        If present, will use this as the filename for context, rather than using the actual filename
+        Useful for temp filenames.
     Returns
     -------
     mediawiki_filename: str
@@ -206,10 +210,7 @@ def convert_hed_xml_2_wiki(hed_xml_url, local_xml_file=None, check_for_issues=Tr
     if local_xml_file is None:
         local_xml_file = file_util.url_to_file(hed_xml_url)
 
-    try:
-        hed_xml_tree = HedSchema.parse_hed_xml_file(local_xml_file)
-    except SchemaFileError as e:
-        return None, e.format_error_message()
+    hed_xml_tree = HedSchema.parse_hed_xml_file(local_xml_file)
 
     xml2wiki = HEDXml2Wiki()
     output_strings = xml2wiki.process_tree(hed_xml_tree)
@@ -217,7 +218,7 @@ def convert_hed_xml_2_wiki(hed_xml_url, local_xml_file=None, check_for_issues=Tr
 
     issue_list = []
     if check_for_issues:
-        warnings = validate_schema(local_xml_file)
+        warnings = validate_schema(local_xml_file, display_filename=display_filename)
         issue_list += warnings
 
     return local_mediawiki_file, issue_list
diff --git a/hedtools/hed/tools/tag_format.py b/hedtools/hed/tools/tag_format.py
index 179ada83b..9bb1825dc 100644
--- a/hedtools/hed/tools/tag_format.py
+++ b/hedtools/hed/tools/tag_format.py
@@ -9,7 +9,7 @@ class TagFormat:
     Class to convert hed3 tags between short and long form.
     """
 
-    def __init__(self, hed_xml_file=None, hed_schema=None):
+    def __init__(self, hed_xml_file=None, hed_schema=None, error_handler=None):
         """
 
         Parameters
@@ -18,9 +18,14 @@ def __init__(self, hed_xml_file=None, hed_schema=None):
             hed xml schema filepath to create HedSchema from
         hed_schema : HedSchema, default None
              Used in place of hed_xml_file
+        error_handler : ErrorHandler or None
+            Used to report errors.  Uses a default one if none passed in.
         """
         if hed_schema is None:
             hed_schema = HedSchema(hed_xml_file)
+        if error_handler is None:
+            error_handler = error_reporter.ErrorHandler()
+        self._error_handler = error_handler
         self._short_tag_mapping = hed_schema.short_tag_mapping
 
     def convert_hed_string_to_short(self, hed_string):
@@ -42,14 +47,14 @@ def convert_hed_string_to_short(self, hed_string):
                 a list of validation errors while converting
         """
         if not self._short_tag_mapping:
-            error = error_reporter.format_schema_error(SchemaErrors.INVALID_SCHEMA, hed_tag=hed_string)
+            error = self._error_handler.format_schema_error(SchemaErrors.INVALID_SCHEMA, hed_tag=hed_string)
             return hed_string, error
 
         hed_string = hed_string_util.remove_slashes_and_spaces(hed_string)
 
         errors = []
         if hed_string == "":
-            errors += error_reporter.format_schema_error(SchemaErrors.EMPTY_TAG_FOUND, "")
+            errors += self._error_handler.format_schema_error(SchemaErrors.EMPTY_TAG_FOUND, "")
             return hed_string, errors
 
         hed_tags = hed_string_util.split_hed_string(hed_string)
@@ -61,7 +66,7 @@ def convert_hed_string_to_short(self, hed_string):
                 short_tag_string, single_error = self._convert_to_short_tag(tag)
                 if single_error:
                     for error in single_error:
-                        errors += error_reporter.reformat_schema_error(error, hed_string, startpos)
+                        errors += self._error_handler.reformat_schema_error(error, hed_string, startpos)
                 final_string += short_tag_string
             else:
                 final_string += tag
@@ -89,14 +94,14 @@ def convert_hed_string_to_long(self, hed_string):
             a list of validation errors while converting
         """
         if not self._short_tag_mapping:
-            error = error_reporter.format_schema_error(SchemaErrors.INVALID_SCHEMA, hed_string)
+            error = self._error_handler.format_schema_error(SchemaErrors.INVALID_SCHEMA, hed_string)
             return hed_string, error
 
         hed_string = hed_string_util.remove_slashes_and_spaces(hed_string)
 
         errors = []
         if hed_string == "":
-            errors += error_reporter.format_schema_error(SchemaErrors.EMPTY_TAG_FOUND, "")
+            errors += self._error_handler.format_schema_error(SchemaErrors.EMPTY_TAG_FOUND, "")
             return hed_string, errors
 
         hed_tags = hed_string_util.split_hed_string(hed_string)
@@ -107,7 +112,7 @@ def convert_hed_string_to_long(self, hed_string):
                 converted_tag, single_error = self._convert_to_long_tag(tag)
                 if single_error:
                     for error in single_error:
-                        errors += error_reporter.reformat_schema_error(error, hed_string, startpos)
+                        errors += self._error_handler.reformat_schema_error(error, hed_string, startpos)
                 final_string += converted_tag
             else:
                 final_string += tag
@@ -172,7 +177,7 @@ def _convert_to_long_tag(self, hed_tag):
                 if tag not in self._short_tag_mapping:
                     found_unknown_extension = True
                     if not found_long_org_tag:
-                        error = error_reporter.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND, hed_tag,
+                        error = self._error_handler.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND, hed_tag,
                                                                    index_start, index_end)
                         return hed_tag, error
                     continue
@@ -183,7 +188,7 @@ def _convert_to_long_tag(self, hed_tag):
 
                 # Verify the tag has the correct path above it.
                 if not tag_string.endswith(main_hed_portion):
-                    error = error_reporter.format_schema_error(SchemaErrors.INVALID_PARENT_NODE, hed_tag,
+                    error = self._error_handler.format_schema_error(SchemaErrors.INVALID_PARENT_NODE, hed_tag,
                                                                index_start, index_end,
                                                                long_org_tag)
                     return hed_tag, error
@@ -192,7 +197,7 @@ def _convert_to_long_tag(self, hed_tag):
             else:
                 # These means we found a known tag in the remainder/extension section, which is an error
                 if tag in self._short_tag_mapping:
-                    error = error_reporter.format_schema_error(SchemaErrors.INVALID_PARENT_NODE, hed_tag,
+                    error = self._error_handler.format_schema_error(SchemaErrors.INVALID_PARENT_NODE, hed_tag,
                                                                index_start, index_end,
                                                                self._short_tag_mapping[tag])
                     return hed_tag, error
@@ -261,7 +266,7 @@ def _convert_to_short_tag(self, hed_tag):
                 index -= 1
 
         if found_long_org_tag is None:
-            error = error_reporter.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND, hed_tag,
+            error = self._error_handler.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND, hed_tag,
                                                        index, last_found_index)
             return hed_tag, error
 
@@ -269,7 +274,7 @@ def _convert_to_short_tag(self, hed_tag):
         main_hed_portion = clean_tag[:last_found_index]
         tag_string = found_long_org_tag.lower()
         if not tag_string.endswith(main_hed_portion):
-            error = error_reporter.format_schema_error(SchemaErrors.INVALID_PARENT_NODE, hed_tag, index,
+            error = self._error_handler.format_schema_error(SchemaErrors.INVALID_PARENT_NODE, hed_tag, index,
                                                        last_found_index, found_long_org_tag)
             return hed_tag, error
 
@@ -278,8 +283,7 @@ def _convert_to_short_tag(self, hed_tag):
         short_tag_string = short_tag + remainder
         return short_tag_string, []
 
-    @staticmethod
-    def _convert_file(input_file, conversion_function):
+    def _convert_file(self, input_file, conversion_function):
         """  Runs a passed in conversion function over a given HedFileInput object
         Parameters
         ----------
@@ -296,14 +300,16 @@ def _convert_file(input_file, conversion_function):
         """
         error_list = []
         for row_number, row_hed_string, column_to_hed_tags_dictionary in input_file:
+            self._error_handler.push_error_context(ErrorContext.ROW, row_number)
             for column_number in column_to_hed_tags_dictionary:
-                error_reporter.push_error_context(ErrorContext.COLUMN, row_number, column_context=column_number)
+                self._error_handler.push_error_context(ErrorContext.COLUMN, column_number)
                 old_text = column_to_hed_tags_dictionary[column_number]
                 new_text, errors = conversion_function(old_text)
                 input_file.set_cell(row_number, column_number, new_text,
                                     include_column_prefix_if_exist=False)
 
-                error_reporter.pop_error_context()
+                self._error_handler.pop_error_context()
+            self._error_handler.pop_error_context()
 
         return input_file, error_list
 
diff --git a/hedtools/hed/util/base_file_input.py b/hedtools/hed/util/base_file_input.py
index 5e5378588..02dd49414 100644
--- a/hedtools/hed/util/base_file_input.py
+++ b/hedtools/hed/util/base_file_input.py
@@ -1,9 +1,12 @@
 import os
 import openpyxl
 import pandas
-from hed.util.column_mapper import ColumnMapper
 import copy
 
+from hed.util.def_dict import DefDict
+from hed.util.column_mapper import ColumnMapper
+from hed.util.exceptions import HedFileError, HedExceptions
+
 
 class BaseFileInput:
     """Handles parsing the actual on disk hed files to a more general format."""
@@ -15,7 +18,8 @@ class BaseFileInput:
     TAB_DELIMITER = '\t'
     COMMA_DELIMITER = ','
 
-    def __init__(self, filename, worksheet_name=None, has_column_names=True, mapper=None):
+    def __init__(self, filename, worksheet_name=None, has_column_names=True, mapper=None,
+                 hed_schema=None):
         """Constructor for the BaseFileInput class.
 
          Parameters
@@ -30,6 +34,8 @@ def __init__(self, filename, worksheet_name=None, has_column_names=True, mapper=
          mapper: ColumnMapper object
              Pass in a built column mapper(see HedFileInput or EventFileInput for examples), or None to just
              retrieve all columns as hed tags.
+         hed_schema: HedSchema object
+             Used to create definitions.
          """
         if mapper is None:
             mapper = ColumnMapper()
@@ -49,6 +55,8 @@ def __init__(self, filename, worksheet_name=None, has_column_names=True, mapper=
             self._dataframe = pandas.read_excel(filename, sheet_name=worksheet_to_load, header=pandas_header)
         elif self.is_text_file():
             self._dataframe = pandas.read_csv(filename, '\t', header=pandas_header)
+        else:
+            raise HedFileError(HedExceptions.INVALID_EXTENSION, "", filename)
 
         # Finalize mapping information if we have columns
         if self._dataframe is not None and self._has_column_names:
@@ -56,7 +64,31 @@ def __init__(self, filename, worksheet_name=None, has_column_names=True, mapper=
             self._mapper.set_column_map(columns)
 
         # Now that the file is fully initialized, gather the definitions from it.
-        mapper.update_definition_mapper_with_file(self)
+        self.file_def_dict, self.file_def_dict_issues = self.extract_definitions(hed_schema)
+        # finally add the new file dict to the mapper.
+        mapper.update_definition_mapper_with_file(self.file_def_dict)
+
+    def extract_definitions(self, hed_schema):
+        """
+        Gathers and validates all definitions found in this spreadsheet
+
+        Parameters
+        ----------
+        hed_schema : HedSchema
+            Used to know where definition tags are located in the schema
+
+        Returns
+        -------
+        def_dict: DefDict
+            Contains all the definitions located in the file
+        validation_issues: [{}]
+            A list of all issues found with the definitions.
+        """
+        new_def_dict = DefDict(hed_schema=hed_schema)
+        validation_issues = []
+        for row_number, row_hed_string, column_to_hed_tags_dictionary in self.iter_raw():
+            validation_issues += new_def_dict.check_for_definitions(row_hed_string)
+        return new_def_dict, validation_issues
 
     def save(self, filename=None, include_formatting=False, output_processed_file=False,
              add_suffix=None):
@@ -260,3 +292,4 @@ def get_worksheet(workbook, worksheet_name=None):
         if not worksheet_name:
             return workbook.worksheets[0]
         return workbook.get_sheet_by_name(worksheet_name)
+
diff --git a/hedtools/hed/util/column_def_group.py b/hedtools/hed/util/column_def_group.py
index 46bdf1636..c4a6ced8e 100644
--- a/hedtools/hed/util/column_def_group.py
+++ b/hedtools/hed/util/column_def_group.py
@@ -1,15 +1,27 @@
 import json
 from hed.util.column_definition import ColumnDef
-from hed.util.error_types import SidecarErrors, ErrorContext
-from hed.util.error_reporter import format_sidecar_error, push_error_context, pop_error_context
+from hed.util.error_types import ErrorContext
+from hed.util import error_reporter
+from hed.util.exceptions import HedFileError, HedExceptions
+from hed.util.def_dict import DefDict
 
 
 class ColumnDefGroup:
     """This stores column definitions for parsing hed spreadsheets, generally loaded from a single json file."""
-    def __init__(self, json_filename=None):
-        self._json_filename = json_filename
+    def __init__(self, json_filename=None, json_string=None):
+        """
+
+        Parameters
+        ----------
+        json_filename: str or None, optional
+            The actual filename to be loaded.
+        json_string: str or None, optional
+            Alternate to passing a filename, you can pass in a json string to be parsed
+        """
+        self._json_filename = None
         self._column_settings = {}
-        self._validation_issues = {}
+        if json_string:
+            self.add_json_string(json_string)
         if json_filename:
             self.add_json_file_defs(json_filename)
 
@@ -22,6 +34,33 @@ def __iter__(self):
         """
         return iter(self._column_settings.values())
 
+    def extract_defs(self, hed_schema, error_handler=None):
+        """
+            Finds all definitions in the hed strings of this column def group.
+
+        Parameters
+        ----------
+        hed_schema : HedSchema
+            Schema to use for checking if definition names are already used, and to locate definition tags.
+        error_handler : ErrorHandler or None
+            Used to report errors.  Uses a default one if none passed in.
+        Returns
+        -------
+        def_dicts: DefDict
+            A DefDict containing all the definitions found
+        validation_issues: [{}]
+            A list of all errors and warnings found while extracting definitions.
+
+        """
+        if error_handler is None:
+            error_handler = error_reporter.ErrorHandler()
+        new_def_dict = DefDict(hed_schema=hed_schema)
+        validation_issues = []
+        for hed_string in self.hed_string_iter():
+            validation_issues += new_def_dict.check_for_definitions(hed_string, error_handler=error_handler)
+
+        return new_def_dict, validation_issues
+
     def save_as_json(self, save_filename):
         """
             Saves out a column definition group to a json file
@@ -36,10 +75,26 @@ def save_as_json(self, save_filename):
         with open(save_filename, "w") as fp:
             json.dump(output_dict, fp, indent=4)
 
+    def add_json_string(self, json_string):
+        """
+            Loads column definitions from a given json string
+
+        Parameters
+        ----------
+        json_string : str
+            path to file to load
+        """
+        try:
+            loaded_defs = json.loads(json_string)
+            for col_def, col_dict in loaded_defs.items():
+                self._add_single_col_type(col_def, col_dict)
+        except json.decoder.JSONDecodeError as e:
+            raise HedFileError(HedExceptions.CANNOT_PARSE_JSON, str(e), json_string[:100])
+
     def add_json_file_defs(self, json_filename):
         """
-            Loads column definitions from a given json file
-            Will add errors related to opening or parsing the file to validation issues.
+            Loads column definitions from a given json file.
+            You can load multiple files into one ColumnDefGroup, but it is discouraged.
 
         Parameters
         ----------
@@ -49,15 +104,15 @@ def add_json_file_defs(self, json_filename):
         try:
             with open(json_filename, "r") as fp:
                 loaded_defs = json.load(fp)
+                self._json_filename = json_filename
                 for col_def, col_dict in loaded_defs.items():
                     self._add_single_col_type(col_def, col_dict)
-
-        except FileNotFoundError:
-            self._validation_issues[json_filename] = format_sidecar_error(SidecarErrors.INVALID_FILENAME,
-                                                                          filename=json_filename)
-        except json.decoder.JSONDecodeError:
-            self._validation_issues[json_filename] = format_sidecar_error(SidecarErrors.CANNOT_PARSE_JSON,
-                                                                          filename=json_filename)
+        except json.decoder.JSONDecodeError as e:
+            raise HedFileError(HedExceptions.CANNOT_PARSE_JSON, str(e), json_filename)
+        except FileNotFoundError as e:
+            raise HedFileError(HedExceptions.FILE_NOT_FOUND, e.strerror, json_filename)
+        except TypeError as e:
+            raise HedFileError(HedExceptions.FILE_NOT_FOUND, str(e), json_filename)
 
     @staticmethod
     def load_multiple_json_files(json_file_input_list):
@@ -65,6 +120,8 @@ def load_multiple_json_files(json_file_input_list):
             Utility function for easily loading multiple json files at once
             This takes a list of filenames or ColumnDefinitionGroups and returns a list of ColumnDefinitionGroups.
 
+            Note: it will completely fail and raise a HedFileError if any of the files are not found.
+
         Parameters
         ----------
         json_file_input_list : [str or ColumnDefinitionGroup]
@@ -84,6 +141,23 @@ def load_multiple_json_files(json_file_input_list):
             loaded_files.append(json_file)
         return loaded_files
 
+    @staticmethod
+    def extract_defs_from_list(column_group_defs, hed_schema):
+        """
+            Take a list of column def groups, and return a list of def dicts extracted from them.
+            This is primarily for quick development tests.  It is not suggested you use this.
+
+        Parameters
+        ----------
+        column_group_defs : [ColumnDefGroup]
+        hed_schema : HedSchema
+        Returns
+        -------
+        def_dicts: [DefDict]
+
+        """
+        return [column_group_def.extract_defs(hed_schema, check_for_issues=False) for column_group_def in column_group_defs]
+
     def hed_string_iter(self, include_position=False):
         """
         Return iterator to loop over all hed strings in all column definitions
@@ -170,47 +244,59 @@ def _add_single_col_type(self, column_name, dict_for_entry, column_type=None):
         column_entry = ColumnDef(column_type, column_name, dict_for_entry)
         self._column_settings[column_name] = column_entry
 
-    def get_validation_issues(self):
-        """Simple getter for previously validated issues from the files.
-
-        Use self.validate_entries to initially validate them.
+    def validate_entries(self, hed_schema=None, display_filename=None, error_handler=None):
+        """Validate the column entries, and also hed strings in the column entries if a hed_schema is passed.
 
+        Parameters
+        ----------
+        hed_schema : HedSchema, optional
+            The dictionary to use to validate individual hed strings.
+        display_filename: str
+            If present, will use this as the filename for context, rather than using the actual filename
+            Useful for temp filenames.
+        error_handler : ErrorHandler or None
+            Used to report errors.  Uses a default one if none passed in.
         Returns
         -------
         validation_issues: [{}]
             The list of validation issues found
+
         """
-        return self._validation_issues
+        if error_handler is None:
+            error_handler = error_reporter.ErrorHandler()
+        if not display_filename:
+            display_filename = self._json_filename
+        if display_filename:
+            error_handler.push_error_context(ErrorContext.FILE_NAME, display_filename, False)
 
-    def validate_entries(self, hed_schema=None, display_filename=None):
-        """Validate the column entries, and also hed strings in the column entries if a hed_schema is passed.
+        all_validation_issues = []
+        for column_entry in self:
+            error_handler.push_error_context(ErrorContext.SIDECAR_COLUMN_NAME, column_entry.column_name)
+            all_validation_issues += column_entry.validate_column_entry(hed_schema, error_handler=error_handler)
+            error_handler.pop_error_context()
+
+        if display_filename:
+            error_handler.pop_error_context()
+        return all_validation_issues
+
+    @staticmethod
+    def get_printable_issue_string(validation_issues, title=None, severity=None, skip_filename=True):
+        """Return a string with issues list flatted into single string, one per line
 
         Parameters
         ----------
-        hed_schema : HedSchema, optional
-            The dictionary to use to validate individual hed strings.
-        display_filename: str
-            If present, it will display errors as coming from this filename instead of the actual source.
-            Useful for temporary files and similar.
+        validation_issues: []
+            Issues to print
+        title: str
+            Optional title that will always show up first if present(even if there are no validation issues)
+        severity: int
+            Return only warnings >= severity
+        skip_filename: bool
+            If true, don't add the filename context to the printable string.
         Returns
         -------
-        validation_issues: [{}]
-            The list of validation issues found
+        str
+            A str containing printable version of the issues or '[]'.
 
         """
-        # If we already have an issue we either already validated, or have a file io error
-        if not self._validation_issues:
-            key_validation_issues = {}
-            if not display_filename:
-                display_filename = self._json_filename
-            push_error_context(ErrorContext.FILE_NAME, display_filename, False)
-            for column_entry in self:
-                push_error_context(ErrorContext.SIDECAR_COLUMN_NAME, column_entry.column_name)
-                col_validation_issues = column_entry.validate_column_entry(hed_schema)
-                if col_validation_issues:
-                    key_validation_issues[column_entry.column_name] = col_validation_issues
-
-                pop_error_context()
-            self._validation_issues = key_validation_issues
-            pop_error_context(False)
-        return self._validation_issues
+        return error_reporter.ErrorHandler.get_printable_issue_string(validation_issues, title, severity, skip_filename)
diff --git a/hedtools/hed/util/column_definition.py b/hedtools/hed/util/column_definition.py
index d8aa19def..b9f03c05a 100644
--- a/hedtools/hed/util/column_definition.py
+++ b/hedtools/hed/util/column_definition.py
@@ -1,7 +1,7 @@
 from enum import Enum
 from hed.util.hed_string_util import split_hed_string, split_hed_string_return_strings
 from hed.util.error_types import SidecarErrors, ErrorContext
-from hed.util.error_reporter import format_sidecar_error, push_error_context, pop_error_context
+from hed.util import error_reporter
 from hed.validator.tag_validator import TagValidator
 
 
@@ -281,7 +281,7 @@ def _detect_column_def_type(dict_for_entry):
 
         return ColumnType.Value
 
-    def validate_column_entry(self, hed_schema=None):
+    def validate_column_entry(self, hed_schema=None, error_handler=None):
         """
         Finds all validation issues in this column.
 
@@ -289,19 +289,22 @@ def validate_column_entry(self, hed_schema=None):
         ----------
         hed_schema : HedSchema, optional
             The dictionary to use to validate hed_strings.  If absent, will only validate column syntax.
-
+        error_handler : ErrorHandler or None
+            Used to report errors.  Uses a default one if none passed in.
         Returns
         -------
         column_issues: [{}]
         """
+        if error_handler is None:
+            error_handler = error_reporter.ErrorHandler()
         col_validation_issues = []
         # Hed string validation can only be done with a hed_schema
         if hed_schema and self.hed_string_iter():
             tag_validator = TagValidator(hed_schema, check_for_warnings=True, run_semantic_validation=True,
-                                         allow_numbers_to_be_pound_sign=True)
+                                         allow_numbers_to_be_pound_sign=True, error_handler=error_handler)
             for hed_string, position in self.hed_string_iter(include_position=True):
-                push_error_context(ErrorContext.SIDECAR_CUE_NAME, position)
-                push_error_context(ErrorContext.SIDECAR_HED_STRING, hed_string, False)
+                error_handler.push_error_context(ErrorContext.SIDECAR_KEY_NAME, position)
+                error_handler.push_error_context(ErrorContext.SIDECAR_HED_STRING, hed_string, False)
                 new_col_validation_issues = tag_validator.run_hed_string_validators(hed_string)
                 for hed_tag in split_hed_string_return_strings(hed_string):
                     hed_tag_lower = hed_tag.lower()
@@ -312,46 +315,47 @@ def validate_column_entry(self, hed_schema=None):
                     col_validation_issues += new_col_validation_issues
                 if self.column_type == ColumnType.Categorical:
                     if hed_string.count("#") != 0:
-                        col_validation_issues += format_sidecar_error(SidecarErrors.TOO_MANY_POUND_SIGNS,
-                                                                      pound_sign_count=hed_string.count("#"))
-                pop_error_context(False)
-                pop_error_context()
+                        col_validation_issues += error_handler.format_sidecar_error(SidecarErrors.TOO_MANY_POUND_SIGNS,
+                                                                                    pound_sign_count=hed_string.count(
+                                                                                        "#"))
+                error_handler.pop_error_context()
+                error_handler.pop_error_context()
 
         if self.column_type is None:
-            col_validation_issues += format_sidecar_error(SidecarErrors.UNKNOWN_COLUMN_TYPE,
-                                                          column_name=self.column_name)
+            col_validation_issues += error_handler.format_sidecar_error(SidecarErrors.UNKNOWN_COLUMN_TYPE,
+                                                                        column_name=self.column_name)
         elif self.column_type == ColumnType.Value:
             raw_hed_dict = self._hed_dict["HED"]
             if not raw_hed_dict:
-                col_validation_issues += format_sidecar_error(SidecarErrors.BLANK_HED_STRING)
+                col_validation_issues += error_handler.format_sidecar_error(SidecarErrors.BLANK_HED_STRING)
             elif not isinstance(raw_hed_dict, str):
-                col_validation_issues += format_sidecar_error(SidecarErrors.WRONG_HED_DATA_TYPE,
-                                                              given_type=type(raw_hed_dict),
-                                                              expected_type="str")
+                col_validation_issues += error_handler.format_sidecar_error(SidecarErrors.WRONG_HED_DATA_TYPE,
+                                                                            given_type=type(raw_hed_dict),
+                                                                            expected_type="str")
             elif raw_hed_dict.count("#") != 1:
-                col_validation_issues += format_sidecar_error(SidecarErrors.INVALID_NUMBER_POUND_SIGNS,
-                                                              pound_sign_count=raw_hed_dict.count("#"))
+                col_validation_issues += error_handler.format_sidecar_error(SidecarErrors.INVALID_NUMBER_POUND_SIGNS,
+                                                                            pound_sign_count=raw_hed_dict.count("#"))
 
         elif self.column_type == ColumnType.Categorical:
             raw_hed_dict = self._hed_dict["HED"]
             if not raw_hed_dict:
-                col_validation_issues += format_sidecar_error(SidecarErrors.BLANK_HED_STRING)
+                col_validation_issues += error_handler.format_sidecar_error(SidecarErrors.BLANK_HED_STRING)
             elif not isinstance(raw_hed_dict, dict):
-                col_validation_issues += format_sidecar_error(SidecarErrors.WRONG_HED_DATA_TYPE,
-                                                              given_type=type(raw_hed_dict),
-                                                              expected_type="dict")
+                col_validation_issues += error_handler.format_sidecar_error(SidecarErrors.WRONG_HED_DATA_TYPE,
+                                                                            given_type=type(raw_hed_dict),
+                                                                            expected_type="dict")
             else:
                 if len(raw_hed_dict) < 2:
                     # Make this a warning
-                    col_validation_issues += format_sidecar_error(SidecarErrors.TOO_FEW_CATEGORIES,
-                                                                  category_count=len(raw_hed_dict))
+                    col_validation_issues += error_handler.format_sidecar_error(SidecarErrors.TOO_FEW_CATEGORIES,
+                                                                                category_count=len(raw_hed_dict))
 
                 # Handle this error here instead of above if we don't have a dictionary for hed_string validation
                 if hed_schema is None:
                     for key, value in raw_hed_dict.items():
                         if value.count("#") != 0:
-                            col_validation_issues += format_sidecar_error(SidecarErrors.TOO_MANY_POUND_SIGNS,
-                                                                          pound_sign_count=value.count("#"))
-
+                            col_validation_issues += error_handler.format_sidecar_error(
+                                SidecarErrors.TOO_MANY_POUND_SIGNS,
+                                pound_sign_count=value.count("#"))
 
         return col_validation_issues
diff --git a/hedtools/hed/util/column_mapper.py b/hedtools/hed/util/column_mapper.py
index 562e493a4..62ef653ef 100644
--- a/hedtools/hed/util/column_mapper.py
+++ b/hedtools/hed/util/column_mapper.py
@@ -9,7 +9,7 @@ class ColumnMapper:
         Private Functions and variables column and row indexing starts at 0.
         Public functions and variables indexing starts at 1(or 2 if has column names)"""
     def __init__(self, json_def_files=None, tag_columns=None, column_prefix_dictionary=None,
-                 hed_schema=None, attribute_columns=None, definition_mapper=None):
+                 attribute_columns=None, definition_mapper=None):
         """Constructor for ColumnMapper
 
         Parameters
@@ -25,13 +25,10 @@ def __init__(self, json_def_files=None, tag_columns=None, column_prefix_dictiona
             4: 'Event/Label/', 5: 'Event/Category/'} The third column contains tags that need Event/Description/
             prepended to them, the fourth column contains tags that need Event/Label/ prepended to them,
             and the fifth column contains tags that needs Event/Category/ prepended to them.
-        hed_schema: HedSchema
-            Used to create a TagValidator, which is then used to validate the entries in value and category entries.
         attribute_columns: str or int or [str] or [int]
              A list of column names or numbers to treat as attributes.
         definition_mapper: DefinitionMapper
             The mapper to use to remove definitions and replace dlabels from the given input
-
         """
         # This points to column_type entries based on column names or indexes if columns have no column_name.
         self.column_defs = {}
@@ -44,7 +41,6 @@ def __init__(self, json_def_files=None, tag_columns=None, column_prefix_dictiona
         self._def_mapper = definition_mapper
 
         self._na_patterns = ["n/a", "nan"]
-        self._hed_schema = hed_schema
 
         if json_def_files:
             self.add_json_file_defs(json_def_files)
@@ -70,17 +66,17 @@ def add_json_file_defs(self, json_file_input_list):
             for column_def in column_group:
                 self._add_column_def(column_def)
 
-    def update_definition_mapper_with_file(self, hed_file_input):
+    def update_definition_mapper_with_file(self, def_dict):
         """
         Adds label definitions gathered from the given list of inputs if this has a definition mapper.
 
         Parameters
         ----------
-        hed_file_input : BaseFileInput
-            The file to gather definitions from.  Generally, this is the same spreadsheet you're opening.
+        def_dict : DefDict
+            The gathered definitions to add to the mapper.
         """
         if self._def_mapper is not None:
-            self._def_mapper.add_definitions(hed_file_input)
+            self._def_mapper.add_definitions(def_dict)
 
     def set_column_prefix_dict(self, column_prefix_dictionary, finalize_mapping=True):
         """Adds the given columns as hed tag columns with the required prefix if it does not already exist.
diff --git a/hedtools/hed/util/def_dict.py b/hedtools/hed/util/def_dict.py
new file mode 100644
index 000000000..e3fd2ff92
--- /dev/null
+++ b/hedtools/hed/util/def_dict.py
@@ -0,0 +1,151 @@
+from hed.util.hed_string_delimiter import HedStringDelimiter
+from hed.util.error_types import DefinitionErrors
+from hed.util import error_reporter
+
+
+class DefDict:
+    """Class responsible for gathering and storing a group of definitions to be considered a single source.
+
+        A bids style file might have many of these(one for each json dict, and another for the actual file)
+    """
+    DLABEL_ORG_KEY = 'Label-def'
+    ELABEL_ORG_KEY = 'Label-exp'
+    DLABEL_KEY = DLABEL_ORG_KEY.lower()
+    ELABEL_KEY = ELABEL_ORG_KEY.lower()
+
+    DEF_KEY = "definition"
+    ORG_KEY = "organizational"
+
+    def __init__(self, hed_schema=None):
+        """
+        Class responsible for gathering and storing a group of definitions to be considered a single source.
+
+        A bids style file might have many of these(one for each json dict, and another for the actual file)
+        Parameters
+        ----------
+        hed_schema : HedSchema, optional
+            Used to determine where definition tags are in the schema.  This is technically optional, but
+            only short form definition tags will work if this is absent.  This is also used to verify definitions
+            are not already used as terms.
+        """
+        self._defs = {}
+
+        if hed_schema:
+            self._def_tag_versions = hed_schema.get_all_forms_of_tag(self.DEF_KEY)
+            self._org_tag_versions = hed_schema.get_all_forms_of_tag(self.ORG_KEY)
+            self._label_tag_versions = hed_schema.get_all_forms_of_tag(self.DLABEL_KEY)
+            if not self._label_tag_versions:
+                self._label_tag_versions = [self.DLABEL_KEY + "/"]
+            self._short_tag_mapping = hed_schema.short_tag_mapping
+        else:
+            self._def_tag_versions = [self.DEF_KEY + "/"]
+            self._org_tag_versions = [self.ORG_KEY + "/"]
+            self._label_tag_versions = [self.DLABEL_KEY + "/"]
+            self._short_tag_mapping = None
+
+    def __iter__(self):
+        return iter(self._defs.items())
+
+    def check_for_definitions(self, hed_string, check_for_issues=True, error_handler=None):
+        """
+        Check a given hed string for definition tags, and add them to the dictionary if so.
+
+        Parameters
+        ----------
+        hed_string : str
+            A single hed string to gather definitions from
+        check_for_issues: bool
+            If we should return validation issues found.(note most issues will be checked for either way,
+            they just won't be returned.)
+        error_handler : ErrorHandler or None
+            Used to report errors.  Uses a default one if none passed in.
+        Returns
+        ----------
+        issues_list: None or [{}]
+            A list of error objects containing warnings and errors related to definitions.
+        """
+        if self.DEF_KEY not in hed_string.lower():
+            return []
+        if error_handler is None:
+            error_handler = error_reporter.ErrorHandler()
+
+        validation_issues = []
+        def_tag_versions = self._def_tag_versions
+        org_tag_versions = self._org_tag_versions
+
+        # This could eventually be replaced with another method as we only care about portions of the tags
+        string_split = HedStringDelimiter(hed_string)
+        for tag_group in string_split.tag_groups:
+            org_tags = []
+            def_tags = []
+            group_tags = []
+            for tag in tag_group:
+                new_def_tag = self._check_tag_starts_with(tag, def_tag_versions)
+                if new_def_tag:
+                    def_tags.append(new_def_tag)
+                    continue
+
+                new_org_tag = self._check_tag_starts_with(tag, org_tag_versions)
+                if new_org_tag:
+                    org_tags.append(tag)
+                    continue
+
+                group_tags.append(tag)
+
+            # Now validate to see if we have a definition.  We want 1 definition, and the other parts are optional.
+            if not def_tags:
+                # If we don't have at least one valid definition tag, just move on.  This is probably a tag with
+                # the word definition somewhere else in the text.
+                continue
+
+            if len(def_tags) > 1:
+                validation_issues += error_handler.format_definition_error(DefinitionErrors.WRONG_NUMBER_DEF_TAGS,
+                                                                           def_name=def_tags[0], tag_list=def_tags[1:])
+                continue
+            def_tag = def_tags[0]
+            if len(org_tags) > 1:
+                validation_issues += error_handler.format_definition_error(DefinitionErrors.WRONG_NUMBER_ORG_TAGS,
+                                                                           def_name=def_tag, tag_list=org_tags)
+                continue
+            if len(group_tags) > 1:
+                validation_issues += error_handler.format_definition_error(DefinitionErrors.WRONG_NUMBER_GROUP_TAGS,
+                                                                           def_name=def_tag, tag_list=group_tags)
+                continue
+
+            org_tag = org_tags[0] if org_tags else None
+            group_tag = group_tags[0] if group_tags else None
+
+            if def_tag in self._defs:
+                validation_issues += error_handler.format_definition_error(DefinitionErrors.DUPLICATE_DEFINITION,
+                                                                           def_name=def_tag)
+            if self._short_tag_mapping and def_tag.lower() in self._short_tag_mapping:
+                validation_issues += error_handler.format_definition_error(DefinitionErrors.TAG_IN_SCHEMA,
+                                                                           def_name=def_tag)
+
+            self._defs[def_tag.lower()] = f"({self.ELABEL_ORG_KEY}/{def_tag}, {org_tag}, {group_tag})"
+
+        if check_for_issues:
+            return validation_issues
+
+    @staticmethod
+    def _check_tag_starts_with(hed_tag, possible_starts_with_list):
+        """ Check if a given tag starts with a given string, and returns the tag with the prefix removed if it does.
+
+        Parameters
+        ----------
+        hed_tag : str
+            A single input tag
+        possible_starts_with_list : list
+            A list of strings to check as the prefix.
+            Generally this will be all short/intermediate/long forms of a specific tag
+            eg. ['definitional', 'informational/definitional', 'attribute/informational/definitional']
+        Returns
+        -------
+            str: the tag without the removed prefix, or None
+        """
+        hed_tag_lower = hed_tag.lower()
+        for start_with_version in possible_starts_with_list:
+            if hed_tag_lower.startswith(start_with_version):
+                found_tag = hed_tag[len(start_with_version):]
+                return found_tag
+        return None
diff --git a/hedtools/hed/util/def_mapper.py b/hedtools/hed/util/def_mapper.py
index ac48b6db2..0c19508f5 100644
--- a/hedtools/hed/util/def_mapper.py
+++ b/hedtools/hed/util/def_mapper.py
@@ -1,7 +1,5 @@
 from hed.util.hed_string_util import split_hed_string
-from hed.util.hed_string_delimiter import HedStringDelimiter
-from hed.util.base_file_input import BaseFileInput
-from hed.util.column_def_group import ColumnDefGroup
+from hed.util.def_dict import DefDict
 
 
 class DefinitionMapper:
@@ -15,57 +13,67 @@ class DefinitionMapper:
     DEF_KEY = "definition"
     ORG_KEY = "organizational"
 
-    def __init__(self, hed_inputs=None, hed_schema=None):
+    def __init__(self, def_dicts=None, hed_schema=None):
         """
+        Class to handle mapping definitions from hed strings to fill them in with their vlaues.
 
         Parameters
         ----------
-        hed_inputs : [] or str or BaseFileInput or ColumnDefGroup
-            List input doesn't need to all be the same type.
+        def_dicts: [DefDict]
+            DefDict's containing all the definitions this mapper should initialize with.  More can be added later.
         hed_schema : HedSchema, optional
             Used to determine where definition tags are in the schema.  This is technically optional, but
             only short form definition tags will work if this is absent.
         """
-        self._defs = {}
-        self._short_tag_mapping = hed_schema.short_tag_mapping
+        self._gathered_defs = {}
 
         if hed_schema:
             self._def_tag_versions = hed_schema.get_all_forms_of_tag(self.DEF_KEY)
-            self._org_tag_versions = hed_schema.get_all_forms_of_tag(self.ORG_KEY)
             self._label_tag_versions = hed_schema.get_all_forms_of_tag(self.DLABEL_KEY)
             if not self._label_tag_versions:
                 self._label_tag_versions = [self.DLABEL_KEY + "/"]
         else:
             self._def_tag_versions = [self.DEF_KEY + "/"]
-            self._org_tag_versions = [self.ORG_KEY + "/"]
             self._label_tag_versions = [self.DLABEL_KEY + "/"]
 
-        if hed_inputs:
-            self.add_definitions(hed_inputs)
+        if def_dicts:
+            self.add_definitions(def_dicts)
 
-    def add_definitions(self, hed_inputs):
+    def add_definitions(self, def_dicts):
         """
         Adds all definitions found in hed strings in the given input
 
         Parameters
         ----------
-        hed_inputs : [] or str or BaseFileInput or ColumnDefGroup
-            List input doesn't need to all be the same type.
+        def_dicts : [] or DefDict
+
         """
-        if not isinstance(hed_inputs, list):
-            hed_inputs = [hed_inputs]
-        for hed_input in hed_inputs:
-            if isinstance(hed_input, str):
-                self._check_for_definitions(hed_input)
-            elif isinstance(hed_input, BaseFileInput):
-                for row_number, row_hed_string, column_to_hed_tags_dictionary in hed_input.iter_raw():
-                    self._check_for_definitions(row_hed_string)
-            elif isinstance(hed_input, ColumnDefGroup):
-                for hed_string in hed_input.hed_string_iter():
-                    self._check_for_definitions(hed_string)
+        if not isinstance(def_dicts, list):
+            def_dicts = [def_dicts]
+        for def_dict in def_dicts:
+            if isinstance(def_dict, DefDict):
+                self._add_definitions_from_group(def_dict)
             else:
-                print(f"Invalid input type '{type(hed_input)} passed to DefinitionMapper.  Skipping.")
-                # add error here
+                print(f"Invalid input type '{type(def_dict)} passed to DefinitionMapper.  Skipping.")
+
+    def _add_definitions_from_group(self, def_dict):
+        """
+            Gather definitions from a single def group and add it to the mapper
+
+        Parameters
+        ----------
+        def_dict : DefDict
+
+        Returns
+        -------
+        """
+        for def_tag, def_value in def_dict:
+            if def_tag in self._gathered_defs:
+                # This is a warning.  Errors from a duplicate definition in a single source will be reported
+                # by DefDict
+                print(f"WARNING: Duplicate definition found for '{def_tag}'.")
+                continue
+            self._gathered_defs[def_tag] = def_value
 
     def replace_and_remove_tags(self, hed_string):
         """Takes a given string and returns the hed string with all definitions removed, and all labels replaced
@@ -113,9 +121,9 @@ def replace_and_remove_tags(self, hed_string):
                 to_remove = full_label_tag
                 label_tag_lower = label_tag.lower()
                 # Just leave definitions in place if they aren't found.
-                if label_tag_lower not in self._defs:
+                if label_tag_lower not in self._gathered_defs:
                     continue
-                replace_with = self._defs[label_tag_lower]
+                replace_with = self._gathered_defs[label_tag_lower]
             else:
                 extents = self._find_tag_group_extent(final_string, tag_index, remove_comma=True)
                 to_remove = hed_string[extents[0]:extents[1]]
@@ -147,67 +155,6 @@ def _check_tag_starts_with(hed_tag, possible_starts_with_list):
                 return found_tag
         return None
 
-    def _check_for_definitions(self, hed_string):
-        """
-        Check a given hed string to see if there is a definition in it, and add it to the definition dict if it does.
-
-        Parameters
-        ----------
-        hed_string : str
-            A single hed string to gather definitions from
-        """
-        if self.DEF_KEY not in hed_string.lower():
-            return False
-
-        def_tag_versions = self._def_tag_versions
-        org_tag_versions = self._org_tag_versions
-
-        # This could eventually be replaced with another method as we only care about portions of the tags
-        string_split = HedStringDelimiter(hed_string)
-        for tag_group in string_split.tag_groups:
-            org_tags = []
-            def_tags = []
-            group_tags = []
-            for tag in tag_group:
-                new_def_tag = DefinitionMapper._check_tag_starts_with(tag, def_tag_versions)
-                if new_def_tag:
-                    def_tags.append(new_def_tag)
-                    continue
-
-                new_org_tag = DefinitionMapper._check_tag_starts_with(tag, org_tag_versions)
-                if new_org_tag:
-                    org_tags.append(tag)
-                    continue
-
-                group_tags.append(tag)
-
-            # Now validate to see if we have a definition.  We want 1 definition, and the other parts are optional.
-            if not def_tags:
-                # If we don't have at least one valid definition tag, just move on.  This is probably a tag with
-                # the word definition somewhere else in the text.
-                continue
-
-            if len(def_tags) > 1:
-                print(f"Too many def tags found in definition for {def_tags[0]}.  Also found: {def_tags[1:]}")
-                continue
-            def_tag = def_tags[0]
-            if len(org_tags) > 1:
-                print(f"Too many org tags found in definition for {def_tag}.  Also found: {org_tags}")
-                continue
-            if len(group_tags) > 1:
-                print(f"Too many group tags found in definition for {def_tag}.  Also found: {group_tags}")
-                continue
-
-            org_tag = org_tags[0] if org_tags else None
-            group_tag = group_tags[0] if group_tags else None
-
-            if def_tag in self._defs:
-                print(f"Duplicate definition found for '{def_tag}'.")
-            if self._short_tag_mapping and def_tag.lower() in self._short_tag_mapping:
-                print(f"Tag '{def_tag}' already in base schema.")
-
-            self._defs[def_tag.lower()] = f"({self.ELABEL_ORG_KEY}/{def_tag}, {org_tag}, {group_tag})"
-
     @staticmethod
     def _find_tag_group_extent(hed_string, starting_tag_index, remove_comma=True):
         """
diff --git a/hedtools/hed/util/error_reporter.py b/hedtools/hed/util/error_reporter.py
index 012b3d0fa..03763c2f1 100644
--- a/hedtools/hed/util/error_reporter.py
+++ b/hedtools/hed/util/error_reporter.py
@@ -5,400 +5,513 @@
 """
 
 from hed.util.error_types import ValidationErrors, ValidationWarnings, SchemaErrors, \
-    SidecarErrors, SchemaWarnings, ErrorContext
-
-# Number of tabs to insert at the start of each line.  Incremented when you push a new error_context.
-current_tab_depth = 0
-# A list of strings containing the current error context scope such as row or column number.
-# Starts from index 0 being the largest(eg file usually) scope.
-error_context_stack = []
-# A copy of the last error_context_stack used to actually format an error.
-# This is used to allow us to only display the context once, regardless of how many errors there are.
-last_used_error_context = []
-
-
-def push_error_context(context_type, context, increment_depth_after=True, column_context=None):
-    """
-    Pushes a new error context to the end of the stack to narrow down error scope.
-
-    Parameters
-    ----------
-    context_type : str
-        This should be a value from ErrorContext representing the type of scope.
-    context : str or int
-        The main value for the context_type.  eg for ErrorContext.FILE_NAME this would be the actual filename.
-    increment_depth_after : bool
-        If True, this adds an extra tab to any subsequent errors in the scope.
-    column_context : str or int
-        This is only used by ErrorContext.COLUMN, where context is the row and column_context is the column number.
-    Returns
-    -------
-    """
-    global current_tab_depth
-    default_tabbing = "\t" * current_tab_depth
-
-    error_types = {
-        ErrorContext.FILE_NAME: f"Errors in file '{context}'",
-        ErrorContext.SIDECAR_COLUMN_NAME: f"Column '{context}':",
-        ErrorContext.SIDECAR_CUE_NAME: f"Cue: {context}",
-        ErrorContext.SIDECAR_HED_STRING: f"hed_string: {context}",
-        ErrorContext.ROW: f'Issues in row {context}:',
-        ErrorContext.COLUMN: f'Issues in row {context} column {column_context}:'
-    }
-
-    default_context = 'ERROR: Unknown error context'
-    context_message = error_types.get(context_type, default_context)
-
-    error_context_stack.append(f"{default_tabbing}{context_message}")
-    if increment_depth_after:
-        current_tab_depth += 1
-
-
-def pop_error_context(decrement_depth_after=True):
-    """
-    Removes the last scope from the error context.
-
-    Parameters
-    ----------
-    decrement_depth_after : bool
-        This should always match the value passed to push_error_context.
-
-    Returns
-    -------
-    """
-    global current_tab_depth
-    if decrement_depth_after:
-        current_tab_depth -= 1
-    error_context_stack.pop()
-
-
-def reset_error_context():
-    """Reset all error context information to defaults
-
-    This function should not be needed with proper usage."""
-    global current_tab_depth
-    current_tab_depth = 0
-    global error_context_stack
-    error_context_stack = []
-    global last_used_error_context
-    last_used_error_context = []
-
-
-def _add_context_to_errors(error_object):
-    """
-    Takes an error object and adds relevant context around it, such as row number, or column name.
-
-    Parameters
-    ----------
-    error_object : {}
-        Generated error containing at least a code and message entry.
-
-    Returns
-    -------
-    error_object_list: [{}]
-        The passed in error with any needed context strings added to the start.
-    """
-    global last_used_error_context
-
-    # The context has not changed, no need to add new context text
-    if last_used_error_context == error_context_stack:
-        return [error_object]
+    SidecarErrors, SchemaWarnings, ErrorContext, ErrorSeverity, DefinitionErrors
+
+
+class ErrorHandler:
+    def __init__(self):
+        # The current (ordered) dictionary of contexts.
+        self.error_context = []
+
+    def push_error_context(self, context_type, context, increment_depth_after=True):
+        """
+        Pushes a new error context to the end of the stack to narrow down error scope.
+
+        Parameters
+        ----------
+        context_type : ErrorContext
+            This should be a value from ErrorContext representing the type of scope.
+        context : str or int
+            The main value for the context_type.  eg for ErrorContext.FILE_NAME this would be the actual filename.
+        increment_depth_after : bool
+            If True, this adds an extra tab to any subsequent errors in the scope.
+        Returns
+        -------
+        """
+        self.error_context.append((context_type, context, increment_depth_after))
+
+    def pop_error_context(self):
+        """
+        Removes the last scope from the error context.
+
+        Returns
+        -------
+        """
+        self.error_context.pop(-1)
+
+    def reset_error_context(self):
+        """Reset all error context information to defaults
+
+        This function should not be needed with proper usage."""
+        self.error_context = []
+
+    def _add_context_to_errors(self, error_object=None):
+        """
+        Takes an error object and adds relevant context around it, such as row number, or column name.
+
+        Parameters
+        ----------
+        error_object : {}
+            Generated error containing at least a code and message entry.
+
+        Returns
+        -------
+        error_object_list: [{}]
+            The passed in error with any needed context strings added to the start.
+        """
+        for (context_type, context, increment_count) in self.error_context:
+            error_object[context_type] = (context, increment_count)
 
-    error_context = []
-    for i, context in enumerate(error_context_stack):
-        if len(last_used_error_context) > i:
-            last_drawn = last_used_error_context[i]
-            # Was drawn, and hasn't changed.
-            if last_drawn == context:
-                continue
+        return [error_object]
 
-        context_object = {'code': 'context', 'message': context}
-        error_context.append(context_object)
-
-    last_used_error_context = error_context_stack.copy()
-
-    return error_context + [error_object]
-
-
-def format_val_error(error_type, hed_string='', tag='', tag_prefix='', previous_tag='',
-                     character='', index=0, unit_class_units='', file_name='', opening_parentheses_count=0,
-                     closing_parentheses_count=0):
-    """Reports the abc error based on the type of error.
-
-    Parameters
-    ----------
-    error_type: str
-        The type of abc error.
-    hed_string: str
-        The full HED string in which the error occurred
-    tag: str
-        The tag that generated the error. The original tag not the formatted one.
-    tag_prefix: str
-        The tag prefix that generated the error.
-    previous_tag: str
-        The previous tag that potentially could have generated the error. This is passed in with the tag.
-    index: int
-        The index in the string of where the error occurred.
-    character: str
-        The character in the string that generated the error.
-    unit_class_units: str
-        The unit class units that are associated with the error.
-    file_name: str
-        The invalid file name that cause the error.
-    opening_parentheses_count: int
-        The number of opening parentheses.
-    closing_parentheses_count: int
-        The number of closing parentheses.
-    Returns
-    -------
-    issue_list: [{}]
-        A list containing a single dictionary with the warning type and warning message related to a particular type
-        of warning.
-
-    """
-    default_tabbing = "\t" * current_tab_depth
-    error_prefix = default_tabbing + "ERROR: "
-    error_types = {
-        ValidationErrors.INVALID_FILENAME: f'Invalid file name - "{file_name}"',
-        ValidationErrors.PARENTHESES: f'{error_prefix}Number of opening and closing parentheses are unequal. '
-                                      f'{opening_parentheses_count} opening parentheses. {closing_parentheses_count} '
-                                      'closing parentheses',
-        ValidationErrors.INVALID_CHARACTER: f'{error_prefix}Invalid character "{character}" at index {index} of string "{hed_string}"',
-        ValidationErrors.COMMA_MISSING: f'{error_prefix}Comma missing after - "{tag}"',
-        ValidationErrors.INVALID_COMMA: f'{error_prefix}Either "{previous_tag}" contains a comma when it should not or "{tag}" is not a valid '
-                                        'tag ',
-        ValidationErrors.DUPLICATE: f'{error_prefix}Duplicate tag - "{tag}"',
-        ValidationErrors.REQUIRE_CHILD: f'{error_prefix}Descendant tag required - "{tag}"',
-        ValidationErrors.EXTRA_TILDE: f'{error_prefix}Too many tildes - group "{tag}"',
-        ValidationErrors.MULTIPLE_UNIQUE: f'{error_prefix}Multiple unique tags with prefix - "{tag_prefix}"',
-        ValidationErrors.UNIT_CLASS_INVALID_UNIT: f'{error_prefix}Invalid unit - "{tag}" valid units are "{unit_class_units}"',
-        ValidationErrors.INVALID_TAG: f'{error_prefix}Invalid tag - "{tag}"',
-        ValidationErrors.EXTRA_DELIMITER: f'{error_prefix}Extra delimiter "{character}" at index {index} of string "{hed_string}"',
-    }
-    default_error_message = 'ERROR: Unknown error'
-    error_message = error_types.get(error_type, default_error_message)
-
-    error_object = {'code': error_type, 'message': error_message}
-    error_object_list = _add_context_to_errors(error_object)
-    return error_object_list
-
-
-def format_sidecar_error(error_type, filename="", column_name="", given_type="", expected_type="", pound_sign_count=0,
-                         category_count=0):
-    """
-    Formats a json sidecar error to human readable
-
-    Parameters
-    ----------
-    error_type : str
-        This should be a value from SidecarErrors
-    filename : str
-        The filename this error is from
-    column_name : str
-        The column name this error is from
-    given_type : str
-        A str representing the type of the variable there is an error with
-    expected_type : str
-        A str representing the expected type of the variable there is an error with
-    pound_sign_count : int
-        The number of pound signs found in the string.
-    category_count : int
-        The number of categories found in this column.
-
-    Returns
-    -------
-    issue_list: [{}]
-        A list containing a single dictionary with the warning type and warning message related to a particular type
-        of warning.
-    """
-    default_tabbing = "\t" * current_tab_depth
-    error_prefix = default_tabbing + "ERROR: "
-    error_types = {
-        SidecarErrors.INVALID_FILENAME: f"{error_prefix}File does not exist or cannot be opened. '{filename}'",
-        SidecarErrors.CANNOT_PARSE_JSON: f"{error_prefix}Json file cannot be parsed. '{filename}'",
-        SidecarErrors.BLANK_HED_STRING: f"{error_prefix}No HED string found for Value or Category column.",
-        SidecarErrors.WRONG_HED_DATA_TYPE: f"{error_prefix}Invalid HED string datatype sidecar. Should be '{expected_type}', but got '{given_type}'",
-        SidecarErrors.INVALID_NUMBER_POUND_SIGNS: f"{error_prefix}There should be exactly one # character in a sidecar string. Found {pound_sign_count}",
-        SidecarErrors.TOO_MANY_POUND_SIGNS: f"{error_prefix}There should be no # characters in a category sidecar string. Found {pound_sign_count}",
-        SidecarErrors.TOO_FEW_CATEGORIES: f"{error_prefix}A category column should have at least two cues. Found {category_count}",
-        SidecarErrors.UNKNOWN_COLUMN_TYPE: f"{error_prefix}Could not automatically identify column '{column_name}' type from file. "
-                                          f"Most likely the column definition in question needs a # sign to replace a number somewhere."
-    }
-
-    default_error_message = f'{error_prefix}Unknown error {error_type}'
-    error_message = error_types.get(error_type, default_error_message)
-
-    error_object = {'code': error_type, 'message': error_message}
-
-    error_object_list = _add_context_to_errors(error_object)
-    return error_object_list
-
-
-def format_val_warning(warning_type, tag='', default_unit='', tag_prefix=''):
-    """Reports the abc warning based on the type of warning.
-
-    Parameters
-    ----------
-    warning_type: string
-        The type of abc warning.
-    tag: string
-        The tag that generated the warning. The original tag not the formatted one.
-    default_unit: string
-        The default unit class unit associated with the warning.
-    tag_prefix: string
-        The tag prefix that generated the warning.
-    Returns
-    -------
-    issue_list: [{}]
-        A list containing a single dictionary with the warning type and warning message related to a particular type
-        of warning.
-
-    """
-    default_tabbing = "\t" * current_tab_depth
-    warning_prefix = default_tabbing + "WARNING: "
-
-    warning_types = {
-        ValidationWarnings.CAPITALIZATION: f'{warning_prefix}First word not capitalized or camel case - "{tag}"',
-        ValidationWarnings.REQUIRED_PREFIX_MISSING: f'{warning_prefix}Tag with prefix "{tag_prefix}" is required',
-        ValidationWarnings.UNIT_CLASS_DEFAULT_USED: f'{warning_prefix}No unit specified. Using "{default_unit}" as the default - "{tag}"'
-    }
-    default_warning_message = 'WARNING: Unknown warning'
-    warning_message = warning_types.get(warning_type, default_warning_message)
-
-    warning_object = {'code': warning_type, 'message': warning_message}
-    warning_object_list = _add_context_to_errors(warning_object)
-    return warning_object_list
-
-
-def reformat_schema_error(error, hed_string, offset):
-    """
-    Updates a schema error from hed_tag indexing to hed_string indexing.
-    
-    Parameters
-    ----------
-    error : {}
-        An error returned from format_schema_error
-    hed_string : str
-        The full hed_string the error is from.
-    offset : int
-        The index into hed_string where the reported error hed_tag started
-    Returns
-    -------
-    {}
-        The same error with index changed if it was a schema error.
-    """
-    # Bail early if this isn't a schema error
-    if "start_index" not in error:
-        return error
-
-    error_type = error["code"]
-    error_index = error["start_index"]
-    error_index_end = error["end_index"]
-    expected_parent_tag = error["expected_parent_tag"]
-
-    reformatted_error = format_schema_error(error_type, hed_string, error_index + offset, error_index_end + offset, expected_parent_tag)
-    return reformatted_error
-
-
-def format_schema_error(error_type, hed_tag, error_index=0, error_index_end=None, expected_parent_tag=None,
-                        duplicate_tag_list=()):
-    """Reports the abc error based on the type of error.
-
-    Parameters
-    ----------
-    error_type: str
-        The type of abc error.
-    hed_tag: str
-        The hed tag with a problem
-    error_index: int
-        The index where the error starts
-    error_index_end: int
-        The index where the error stops
-    expected_parent_tag: str
-        The expected full path of the tag from the schema.
-    duplicate_tag_list: []
-        A list of all all possible parents for this tag, if there is more than one.
-    Returns
-    -------
-    issue_list: [{}]
-        A list containing a single dictionary with the warning type and warning message related to a particular type
-        of warning.
-
-    """
-    if error_index_end is None:
-        error_index_end = len(hed_tag)
-
-    problem_tag = hed_tag[error_index: error_index_end]
-
-    default_tabbing = "\t" * current_tab_depth
-    error_prefix = f"{default_tabbing}ERROR: "
-
-    tag_join_delimiter = f"\n\t{default_tabbing}"
-    error_types = {
-        SchemaErrors.INVALID_PARENT_NODE: f"{error_prefix}'{problem_tag}' appears as '{expected_parent_tag}' and cannot be used "
-                                          f"as an extension.  {error_index}, {error_index_end}",
-        SchemaErrors.NO_VALID_TAG_FOUND: f"{error_prefix}'{problem_tag}' is not a valid base hed tag.  {error_index}, {error_index_end} ",
-        SchemaErrors.EMPTY_TAG_FOUND: f"{error_prefix}Empty tag cannot be converted.",
-        SchemaErrors.INVALID_SCHEMA: f"{error_prefix}Source hed schema is invalid as it contains duplicate tags.  "
-                                     f"Please fix if you wish to be abe to convert tags. {error_index}, {error_index_end}",
-        SchemaErrors.DUPLICATE_TERMS: f"{error_prefix}Term(Short Tag) '{hed_tag}' used {len(duplicate_tag_list)} places in schema as: {tag_join_delimiter}"
-                                      f"{tag_join_delimiter.join(duplicate_tag_list)}"
-    }
-    default_error_message = f'{error_prefix}Internal Error'
-    error_message = error_types.get(error_type, default_error_message)
-
-    error_object = {'code': error_type,
-                    'message': error_message,
-                    'source_tag': hed_tag,
-                    'start_index': error_index,
-                    'end_index': error_index_end,
-                    'expected_parent_tag': expected_parent_tag}
-
-    error_object_list = _add_context_to_errors(error_object)
-    return error_object_list
-
-
-def format_schema_warning(error_type, hed_tag, hed_desc=None, error_index=0, problem_char=None):
-    """Reports the abc warning based on the type of error.
-
-    Parameters
-    ----------
-    error_type: str
-        The type of abc error.
-    hed_tag: str
-        The hed tag this error is from
-    hed_desc: str
-        The description this error is from
-    error_index: int
-        The position of the character with a problem
-    problem_char: char
-        The invalid character
-    Returns
-    -------
-    issue_list: [{}]
-        A list containing a single dictionary with the warning type and warning message related to a particular type
-        of warning.
-
-    """
-    problem_tag = hed_tag
-
-    default_tabbing = "\t" * current_tab_depth
-    warning_prefix = f"{default_tabbing}WARNING: "
-    error_types = {
-        SchemaWarnings.INVALID_CHARACTERS_IN_DESC: f"{warning_prefix}Invalid character '{problem_char}' in desc "
-                                                   f"for '{problem_tag}' at position {error_index}.  '{hed_desc}",
-        SchemaWarnings.INVALID_CHARACTERS_IN_TAG: f"{warning_prefix}Invalid character '{problem_char}' in tag "
+    def format_val_error(self, error_type, hed_string='', tag='', tag_prefix='', previous_tag='',
+                         character='', index=0, unit_class_units='', opening_parentheses_count=0,
+                         closing_parentheses_count=0):
+        """Reports the abc error based on the type of error.
+
+        Parameters
+        ----------
+        error_type: str
+            The type of abc error.
+        hed_string: str
+            The full HED string in which the error occurred
+        tag: str
+            The tag that generated the error. The original tag not the formatted one.
+        tag_prefix: str
+            The tag prefix that generated the error.
+        previous_tag: str
+            The previous tag that potentially could have generated the error. This is passed in with the tag.
+        index: int
+            The index in the string of where the error occurred.
+        character: str
+            The character in the string that generated the error.
+        unit_class_units: str
+            The unit class units that are associated with the error.
+        opening_parentheses_count: int
+            The number of opening parentheses.
+        closing_parentheses_count: int
+            The number of closing parentheses.
+        Returns
+        -------
+        issue_list: [{}]
+            A list containing a single dictionary with the warning type and warning message related to a particular type
+            of warning.
+
+        """
+        error_prefix = "ERROR: "
+        error_types = {
+            ValidationErrors.PARENTHESES: f'{error_prefix}Number of opening and closing parentheses are unequal. '
+                                          f'{opening_parentheses_count} opening parentheses. {closing_parentheses_count} '
+                                          'closing parentheses',
+            ValidationErrors.INVALID_CHARACTER: f'{error_prefix}Invalid character "{character}" at index {index} of string "{hed_string}"',
+            ValidationErrors.COMMA_MISSING: f'{error_prefix}Comma missing after - "{tag}"',
+            ValidationErrors.INVALID_COMMA: f'{error_prefix}Either "{previous_tag}" contains a comma when it should not or "{tag}" is not a valid '
+                                            'tag ',
+            ValidationErrors.DUPLICATE: f'{error_prefix}Duplicate tag - "{tag}"',
+            ValidationErrors.REQUIRE_CHILD: f'{error_prefix}Descendant tag required - "{tag}"',
+            ValidationErrors.EXTRA_TILDE: f'{error_prefix}Too many tildes - group "{tag}"',
+            ValidationErrors.MULTIPLE_UNIQUE: f'{error_prefix}Multiple unique tags with prefix - "{tag_prefix}"',
+            ValidationErrors.UNIT_CLASS_INVALID_UNIT: f'{error_prefix}Invalid unit - "{tag}" valid units are "{unit_class_units}"',
+            ValidationErrors.INVALID_TAG: f'{error_prefix}Invalid tag - "{tag}"',
+            ValidationErrors.EXTRA_DELIMITER: f'{error_prefix}Extra delimiter "{character}" at index {index} of string "{hed_string}"',
+        }
+        default_error_message = 'ERROR: Unknown error'
+        error_message = error_types.get(error_type, default_error_message)
+
+        error_object = {'code': error_type, 'message': error_message, 'severity': ErrorSeverity.ERROR}
+        error_object_list = self._add_context_to_errors(error_object)
+        return error_object_list
+
+    def format_sidecar_error(self, error_type, column_name="", given_type="", expected_type="", pound_sign_count=0,
+                             category_count=0):
+        """
+        Formats a json sidecar error to human readable
+
+        Parameters
+        ----------
+        error_type : str
+            This should be a value from SidecarErrors
+        column_name : str
+            The column name this error is from
+        given_type : str
+            A str representing the type of the variable there is an error with
+        expected_type : str
+            A str representing the expected type of the variable there is an error with
+        pound_sign_count : int
+            The number of pound signs found in the string.
+        category_count : int
+            The number of categories found in this column.
+
+        Returns
+        -------
+        issue_list: [{}]
+            A list containing a single dictionary with the warning type and warning message related to a particular type
+            of warning.
+        """
+        error_prefix = "ERROR: "
+        error_types = {
+            SidecarErrors.BLANK_HED_STRING: f"{error_prefix}No HED string found for Value or Category column.",
+            SidecarErrors.WRONG_HED_DATA_TYPE: f"{error_prefix}Invalid HED string datatype sidecar. Should be '{expected_type}', but got '{given_type}'",
+            SidecarErrors.INVALID_NUMBER_POUND_SIGNS: f"{error_prefix}There should be exactly one # character in a sidecar string. Found {pound_sign_count}",
+            SidecarErrors.TOO_MANY_POUND_SIGNS: f"{error_prefix}There should be no # characters in a category sidecar string. Found {pound_sign_count}",
+            SidecarErrors.TOO_FEW_CATEGORIES: f"{error_prefix}A category column should have at least two cues. Found {category_count}",
+            SidecarErrors.UNKNOWN_COLUMN_TYPE: f"{error_prefix}Could not automatically identify column '{column_name}' type from file. "
+                                              f"Most likely the column definition in question needs a # sign to replace a number somewhere."
+        }
+
+        default_error_message = f'{error_prefix}Unknown error {error_type}'
+        error_message = error_types.get(error_type, default_error_message)
+
+        error_object = {'code': error_type, 'message': error_message, 'severity': ErrorSeverity.ERROR}
+
+        error_object_list = self._add_context_to_errors(error_object)
+        return error_object_list
+
+    def format_val_warning(self, warning_type, tag='', default_unit='', tag_prefix=''):
+        """Reports the abc warning based on the type of warning.
+
+        Parameters
+        ----------
+        warning_type: string
+            The type of abc warning.
+        tag: string
+            The tag that generated the warning. The original tag not the formatted one.
+        default_unit: string
+            The default unit class unit associated with the warning.
+        tag_prefix: string
+            The tag prefix that generated the warning.
+        Returns
+        -------
+        issue_list: [{}]
+            A list containing a single dictionary with the warning type and warning message related to a particular type
+            of warning.
+
+        """
+        warning_prefix = "WARNING: "
+
+        warning_types = {
+            ValidationWarnings.CAPITALIZATION: f'{warning_prefix}First word not capitalized or camel case - "{tag}"',
+            ValidationWarnings.REQUIRED_PREFIX_MISSING: f'{warning_prefix}Tag with prefix "{tag_prefix}" is required',
+            ValidationWarnings.UNIT_CLASS_DEFAULT_USED: f'{warning_prefix}No unit specified. Using "{default_unit}" as the default - "{tag}"'
+        }
+        default_warning_message = 'WARNING: Unknown warning'
+        warning_message = warning_types.get(warning_type, default_warning_message)
+
+        warning_object = {'code': warning_type, 'message': warning_message, 'severity': ErrorSeverity.WARNING}
+        warning_object_list = self._add_context_to_errors(warning_object)
+        return warning_object_list
+
+    def reformat_schema_error(self, error, hed_string, offset):
+        """
+        Updates a schema error from hed_tag indexing to hed_string indexing.
+
+        Parameters
+        ----------
+        error : {}
+            An error returned from format_schema_error
+        hed_string : str
+            The full hed_string the error is from.
+        offset : int
+            The index into hed_string where the reported error hed_tag started
+        Returns
+        -------
+        {}
+            The same error with index changed if it was a schema error.
+        """
+        # Bail early if this isn't a schema error
+        if "start_index" not in error:
+            return error
+
+        error_type = error["code"]
+        error_index = error["start_index"]
+        error_index_end = error["end_index"]
+        expected_parent_tag = error["expected_parent_tag"]
+
+        reformatted_error = self.format_schema_error(error_type, hed_string, error_index + offset, error_index_end + offset, expected_parent_tag)
+        return reformatted_error
+
+    def format_schema_error(self, error_type, hed_tag, error_index=0, error_index_end=None, expected_parent_tag=None,
+                            duplicate_tag_list=()):
+        """Reports the abc error based on the type of error.
+
+        Parameters
+        ----------
+        error_type: str
+            The type of abc error.
+        hed_tag: str
+            The hed tag with a problem
+        error_index: int
+            The index where the error starts
+        error_index_end: int
+            The index where the error stops
+        expected_parent_tag: str
+            The expected full path of the tag from the schema.
+        duplicate_tag_list: []
+            A list of all all possible parents for this tag, if there is more than one.
+        Returns
+        -------
+        issue_list: [{}]
+            A list containing a single dictionary with the warning type and warning message related to a particular type
+            of warning.
+
+        """
+        if error_index_end is None:
+            error_index_end = len(hed_tag)
+
+        problem_tag = hed_tag[error_index: error_index_end]
+
+        error_prefix = f"ERROR: "
+
+        tag_join_delimiter = f"\n\t"
+        error_types = {
+            SchemaErrors.INVALID_PARENT_NODE: f"{error_prefix}'{problem_tag}' appears as '{expected_parent_tag}' and cannot be used "
+                                              f"as an extension.  {error_index}, {error_index_end}",
+            SchemaErrors.NO_VALID_TAG_FOUND: f"{error_prefix}'{problem_tag}' is not a valid base hed tag.  {error_index}, {error_index_end} ",
+            SchemaErrors.EMPTY_TAG_FOUND: f"{error_prefix}Empty tag cannot be converted.",
+            SchemaErrors.INVALID_SCHEMA: f"{error_prefix}Source hed schema is invalid as it contains duplicate tags.  "
+                                         f"Please fix if you wish to be abe to convert tags. {error_index}, {error_index_end}",
+            SchemaErrors.DUPLICATE_TERMS: f"{error_prefix}Term(Short Tag) '{hed_tag}' used {len(duplicate_tag_list)} places in schema as: {tag_join_delimiter}"
+                                          f"{tag_join_delimiter.join(duplicate_tag_list)}"
+        }
+        default_error_message = f'{error_prefix}Internal Error'
+        error_message = error_types.get(error_type, default_error_message)
+
+        error_object = {'code': error_type,
+                        'message': error_message,
+                        'source_tag': hed_tag,
+                        'start_index': error_index,
+                        'end_index': error_index_end,
+                        'expected_parent_tag': expected_parent_tag,
+                        'severity': ErrorSeverity.ERROR}
+
+        error_object_list = self._add_context_to_errors(error_object)
+        return error_object_list
+
+    def format_schema_warning(self, error_type, hed_tag, hed_desc=None, error_index=0, problem_char=None):
+        """Reports the abc warning based on the type of error.
+
+        Parameters
+        ----------
+        error_type: str
+            The type of abc error.
+        hed_tag: str
+            The hed tag this error is from
+        hed_desc: str
+            The description this error is from
+        error_index: int
+            The position of the character with a problem
+        problem_char: char
+            The invalid character
+        Returns
+        -------
+        issue_list: [{}]
+            A list containing a single dictionary with the warning type and warning message related to a particular type
+            of warning.
+
+        """
+        problem_tag = hed_tag
+
+        warning_prefix = f"WARNING: "
+        error_types = {
+            SchemaWarnings.INVALID_CHARACTERS_IN_DESC: f"{warning_prefix}Invalid character '{problem_char}' in desc "
+                                                       f"for '{problem_tag}' at position {error_index}.  '{hed_desc}",
+            SchemaWarnings.INVALID_CHARACTERS_IN_TAG: f"{warning_prefix}Invalid character '{problem_char}' in tag "
+                                                       f"'{problem_tag}' at position {error_index}.",
+            SchemaWarnings.INVALID_CAPITALIZATION: f"{warning_prefix}First character just be a capital letter or number.  "
+                                                   f"Found character '{problem_char}' in tag "
                                                    f"'{problem_tag}' at position {error_index}.",
-        SchemaWarnings.INVALID_CAPITALIZATION: f"{warning_prefix}First character just be a capital letter or number.  "
-                                               f"Found character '{problem_char}' in tag "
-                                               f"'{problem_tag}' at position {error_index}.",
-    }
-    default_warning_message = f'{warning_prefix}Internal Error'
-    warning_message = error_types.get(error_type, default_warning_message)
-
-    error_object = {'code': error_type,
-                    'message': warning_message,
-                    'source_tag': hed_tag}
-
-    error_object_list = _add_context_to_errors(error_object)
-    return error_object_list
+        }
+        default_warning_message = f'{warning_prefix}Internal Error'
+        warning_message = error_types.get(error_type, default_warning_message)
+
+        error_object = {'code': error_type,
+                        'message': warning_message,
+                        'source_tag': hed_tag,
+                        'severity': ErrorSeverity.WARNING}
+
+        error_object_list = self._add_context_to_errors(error_object)
+        return error_object_list
+
+    def format_definition_error(self, error_type, def_name, tag_list=None):
+        error_prefix = f"ERROR: "
+        error_types = {
+            DefinitionErrors.WRONG_NUMBER_DEF_TAGS:
+                f"{error_prefix}Too many def tags found in definition for {def_name}.  Expected 1, also found: {tag_list}",
+            DefinitionErrors.WRONG_NUMBER_ORG_TAGS:
+                f"{error_prefix}Too many org tags found in definition for {def_name}.  Expected 0 or 1, found: {tag_list}",
+            DefinitionErrors.WRONG_NUMBER_GROUP_TAGS:
+                f"{error_prefix}Too many group tags found in definition for {def_name}.  Expected 1, found: {tag_list}",
+            DefinitionErrors.DUPLICATE_DEFINITION:
+                f"{error_prefix}Duplicate definition found for '{def_name}'.",
+            DefinitionErrors.TAG_IN_SCHEMA:
+                f"{error_prefix}Term '{def_name}' already used as term in schema and cannot be re-used as a definition."
+        }
+        default_error_message = f'{error_prefix}Internal Error'
+        error_message = error_types.get(error_type, default_error_message)
+
+        error_object = {'code': error_type,
+                        'message': error_message,
+                        'severity': ErrorSeverity.ERROR}
+
+        error_object_list = self._add_context_to_errors(error_object)
+        return error_object_list
+
+    @staticmethod
+    def _get_context_from_issue(val_issue, skip_filename=True):
+        """
+        Extract all the context values from the given issue
+        Parameters
+        ----------
+        val_issue : {}
+            A dictionary a representing a single error
+        skip_filename: bool
+            If true, don't gather the filename context.
+        Returns
+        -------
+        context_list: []
+            A list of tuples containing the context_type and context for the given issue
+        """
+        single_issue_context = []
+        for key in val_issue:
+            if skip_filename and key == ErrorContext.FILE_NAME:
+                continue
+            if key in ErrorContext:
+                single_issue_context.append((key.value, *val_issue[key]))
+
+        return single_issue_context
+
+    @staticmethod
+    def _format_single_context_string(context_type, context, tab_count=0):
+        """
+        Takes a single context tuple and returns the human readable form.
+
+        Parameters
+        ----------
+        context_type : str
+            The context type of this entry
+        context : str
+            The value of this context
+        tab_count : int
+            Number of tabs to prefix each line with.
+
+        Returns
+        -------
+        context_string: str
+            A string containing the context, including tabs.
+        """
+        tab_string = tab_count * '\t'
+        error_types = {
+            ErrorContext.FILE_NAME.value: f"\nErrors in file '{context}'",
+            ErrorContext.SIDECAR_COLUMN_NAME.value: f"Column '{context}':",
+            ErrorContext.SIDECAR_KEY_NAME.value: f"Key: {context}",
+            ErrorContext.SIDECAR_HED_STRING.value: f"hed_string: {context}",
+            ErrorContext.ROW.value: f'Issues in row {context}:',
+            ErrorContext.COLUMN.value: f'Issues in column {context}:',
+            ErrorContext.CUSTOM_TITLE.value: context
+        }
+        context_portion = error_types[context_type]
+        context_string = f"{tab_string}{context_portion}\n"
+        return context_string
+
+    @staticmethod
+    def _get_context_string(single_issue_context, last_used_context):
+        """
+        Converts a single context list into the final human readable output form.
+
+        Parameters
+        ----------
+        single_issue_context : [()]
+            A list of tuples containing the context(context_type, context, increment_tab)
+        last_used_context : [()]
+            A list of tuples containing the last drawn context, so it can only add the parts that have changed.
+            This is always the same format as single_issue_context.
+
+        Returns
+        -------
+        context_string: str
+            The full string of context(potentially multiline) to add before the error
+        tab_string: str
+            The prefix to add to any message line with this context.
+        """
+        context_string = ""
+        tab_count = 0
+        found_difference = False
+        for i, context_tuple in enumerate(single_issue_context):
+            (context_type, context, increment_tab) = context_tuple
+            if len(last_used_context) > i and not found_difference:
+                last_drawn = last_used_context[i]
+                # Was drawn, and hasn't changed.
+                if last_drawn == context_tuple:
+                    if increment_tab:
+                        tab_count += 1
+                    continue
+
+            context_string += ErrorHandler._format_single_context_string(context_type, context, tab_count)
+            found_difference = True
+            if increment_tab:
+                tab_count += 1
+
+        tab_string = '\t' * tab_count
+        return context_string, tab_string
+
+    @staticmethod
+    def get_printable_issue_string(validation_issues, title=None, severity=None, skip_filename=True):
+        """Return a string with issues list flatted into single string, one per line
+
+        Parameters
+        ----------
+        validation_issues: []
+            Issues to print
+        title: str
+            Optional title that will always show up first if present(even if there are no validation issues)
+        severity: int
+            Return only warnings >= severity
+        skip_filename: bool
+            If true, don't add the filename context to the printable string.
+        Returns
+        -------
+        str
+            A str containing printable version of the issues or '[]'.
+
+        """
+        last_used_error_context = []
+
+        if severity is not None:
+            validation_issues = ErrorHandler.filter_issues_by_severity(validation_issues, severity)
+
+        issue_string = ""
+        for single_issue in validation_issues:
+            single_issue_context = ErrorHandler._get_context_from_issue(single_issue, skip_filename)
+            context_string, tab_string = ErrorHandler._get_context_string(single_issue_context, last_used_error_context)
+
+            issue_string += context_string
+            single_issue_message = tab_string + single_issue['message']
+            if "\n" in single_issue_message:
+                single_issue_message = single_issue_message.replace("\n", "\n" + tab_string)
+            issue_string += f"{single_issue_message}\n"
+            last_used_error_context = single_issue_context.copy()
+
+        if issue_string:
+            issue_string += "\n"
+        if title:
+            issue_string = title + '\n' + issue_string
+        return issue_string
+
+    @staticmethod
+    def filter_issues_by_severity(issues_list, severity):
+        """
+        Gathers all issues matching or below a given severity.
+            
+        Parameters
+        ----------
+        issues_list : [{}]
+            The full issue list
+        severity : int
+            The level of issue you're interested in
+
+        Returns
+        -------
+        filtered_issues_list: [{}]
+            The list with all other severities removed.
+        """
+        return [issue for issue in issues_list if issue['severity'] <= severity]
diff --git a/hedtools/hed/util/error_types.py b/hedtools/hed/util/error_types.py
index 3a8fd1408..6dbe815da 100644
--- a/hedtools/hed/util/error_types.py
+++ b/hedtools/hed/util/error_types.py
@@ -1,17 +1,25 @@
+from enum import Enum
 
-class ErrorContext:
+
+class ErrorSeverity:
+    ERROR = 1
+    WARNING = 2
+
+
+class ErrorContext(Enum):
     """Indicates the context this error took place in, each error potentially having multiple contexts"""
+    CUSTOM_TITLE = 'title'
     FILE_NAME = 'filename'
     SIDECAR_COLUMN_NAME = 'sidecarColumnName'
-    SIDECAR_CUE_NAME = 'sidecarCueName'
+    SIDECAR_KEY_NAME = 'sidecarKeyName'
     SIDECAR_HED_STRING = 'sidecarHedString'
     ROW = 'row'
     COLUMN = 'column'
+    # Use this one to display any passed in message without modification
 
 
 class ValidationErrors:
     # General validation errors
-    INVALID_FILENAME = 'invalidFileName'
     UNIT_CLASS_INVALID_UNIT = 'unitClassInvalidUnit'
     EXTRA_DELIMITER = 'extraDelimiter'
     INVALID_CHARACTER = 'invalidCharacter'
@@ -33,8 +41,6 @@ class ValidationWarnings:
 
 class SidecarErrors:
     # These are for json sidecar validation errors(sidecars can also produce most normal validation errors)
-    INVALID_FILENAME = 'sidecarInvalidFilename'
-    CANNOT_PARSE_JSON = 'cannotParseJson'
     BLANK_HED_STRING = 'blankValueString'
     WRONG_HED_DATA_TYPE = 'wrongHedDataType'
     INVALID_NUMBER_POUND_SIGNS = 'invalidNumberPoundSigns'
@@ -49,8 +55,6 @@ class SchemaErrors:
     NO_VALID_TAG_FOUND = "invalidTag"
     INVALID_SCHEMA = 'invalidSchema'
     EMPTY_TAG_FOUND = 'emptyTag'
-    FILE_NOT_FOUND = 'fileNotFound'
-    CANNOT_PARSE_XML = 'cannotParseXML'
     DUPLICATE_TERMS = 'duplicateTerms'
 
 
@@ -58,3 +62,11 @@ class SchemaWarnings:
     INVALID_CHARACTERS_IN_DESC = "invalidCharDesc"
     INVALID_CHARACTERS_IN_TAG = "invalidCharTag"
     INVALID_CAPITALIZATION = 'invalidCaps'
+
+
+class DefinitionErrors:
+    WRONG_NUMBER_DEF_TAGS = 'wrongNumberDefTags'
+    WRONG_NUMBER_ORG_TAGS = 'wrongNumberOrgTags'
+    WRONG_NUMBER_GROUP_TAGS = 'wrongNumberGroupTags'
+    DUPLICATE_DEFINITION = 'duplicateDefinition'
+    TAG_IN_SCHEMA = 'defAlreadyInSchema'
diff --git a/hedtools/hed/util/event_file_input.py b/hedtools/hed/util/event_file_input.py
index 7efb9a86b..5496105f2 100644
--- a/hedtools/hed/util/event_file_input.py
+++ b/hedtools/hed/util/event_file_input.py
@@ -2,6 +2,7 @@
 from hed.util.base_file_input import BaseFileInput
 from hed.util.column_def_group import ColumnDefGroup
 from hed.util.def_mapper import DefinitionMapper
+from hed.util import error_reporter
 
 
 class EventFileInput(BaseFileInput):
@@ -10,7 +11,7 @@ class EventFileInput(BaseFileInput):
     def __init__(self, filename, worksheet_name=None, tag_columns=None,
                  has_column_names=True, column_prefix_dictionary=None,
                  json_def_files=None, attribute_columns=None,
-                 hed_schema=None):
+                 hed_schema=None, def_dicts=None):
         """Constructor for the EventFileInput class.
 
         Parameters
@@ -37,6 +38,10 @@ def __init__(self, filename, worksheet_name=None, tag_columns=None,
             Default: ["duration", "onset"]
         hed_schema: HedSchema
            Used by column mapper to do (optional) hed string validation, and also to gather definition tags correctly.
+        def_dicts: [DefDict]
+            DefDict's containing all the definitions this file should use - other than the ones coming from the file
+            itself.
+            If this is NOT passed, the class will instead gather definitions from any passed in ColumnDefGroups
         """
         if tag_columns is None:
             tag_columns = []
@@ -45,12 +50,21 @@ def __init__(self, filename, worksheet_name=None, tag_columns=None,
         if attribute_columns is None:
             attribute_columns = ["duration", "onset"]
 
+        # This stuff is mostly here to make this easier to use for development
+        # you should generally create the DefDict and ColumnDefGroup before reaching this point.
         self.column_group_defs = ColumnDefGroup.load_multiple_json_files(json_def_files)
+        if def_dicts is None:
+            self.def_dicts = ColumnDefGroup.extract_defs_from_list(self.column_group_defs, hed_schema)
+        else:
+            if not isinstance(def_dicts, list):
+                self.def_dicts = [def_dicts]
+            else:
+                self.def_dicts = def_dicts
 
-        def_mapper = DefinitionMapper(self.column_group_defs, hed_schema=hed_schema)
+        def_mapper = DefinitionMapper(self.def_dicts, hed_schema=hed_schema)
         new_mapper = ColumnMapper(json_def_files=self.column_group_defs, tag_columns=tag_columns,
                                   column_prefix_dictionary=column_prefix_dictionary,
-                                  hed_schema=hed_schema, attribute_columns=attribute_columns,
+                                  attribute_columns=attribute_columns,
                                   definition_mapper=def_mapper)
 
         super().__init__(filename, worksheet_name, has_column_names, new_mapper)
@@ -59,23 +73,28 @@ def __init__(self, filename, worksheet_name=None, tag_columns=None,
             raise ValueError("You are attempting to open a bids style file with no column headers provided.\n"
                              "This is probably not intended.")
 
-    def validate_file_sidecars(self, hed_schema=None):
+    def validate_file_sidecars(self, hed_schema=None, error_handler=None):
         """
         Validates all column definitions and column definition hed strings.
 
+        This is not an encouraged way to do this.  You should instead validate the sidecars BEFORE creating
+        an EventFileInput
         Parameters
         ----------
         hed_schema : HedSchema, optional
             Also semantically validates hed strings if present.
+        error_handler : ErrorHandler or None
+            Used to report errors.  Uses a default one if none passed in.
         Returns
         -------
         validation_issues : [{}]
             A list of syntax and semantic issues found in the definitions.
         """
-        validation_issues = []
+        if error_handler is None:
+            error_handler = error_reporter.ErrorHandler()
+        all_validation_issues = []
         for column_def_group in self.column_group_defs:
-            new_issue_dict = column_def_group.validate_entries(hed_schema=hed_schema)
-            for name, issues in new_issue_dict.items():
-                validation_issues += issues
+            all_validation_issues += column_def_group.validate_entries(hed_schema=hed_schema,
+                                                                       error_handler=error_handler)
 
-        return validation_issues
+        return all_validation_issues
diff --git a/hedtools/hed/util/exceptions.py b/hedtools/hed/util/exceptions.py
index 204574bef..651577372 100644
--- a/hedtools/hed/util/exceptions.py
+++ b/hedtools/hed/util/exceptions.py
@@ -1,18 +1,21 @@
-from hed.util.error_types import SchemaErrors
+class HedExceptions:
+    # A list of all exceptions that can be generated by the hedtools.
+    FILE_NOT_FOUND = 'fileNotFound'
+    CANNOT_PARSE_XML = 'cannotParseXML'
+    CANNOT_PARSE_JSON = 'cannotParseJson'
+    INVALID_EXTENSION = 'invalidExtension'
 
 
-class SchemaFileError(Exception):
-    """Exception raised when a schema XML file cannot be parsed due to being malformed, file IO, etc."""
+class HedFileError(Exception):
+    """Exception raised when a file cannot be parsed due to being malformed, file IO, etc."""
     def __init__(self, error_type, message, filename):
         self.error_type = error_type
         self.message = message
         self.filename = filename
 
-    SCHEMA_ERROR_PREFIX = "ERROR: "
-
     def format_error_message(self, include_tabbing=True, return_string_only=False,
                              display_filename=None):
-        """This takes a SchemaFileError exception and translates it to human readable
+        """This takes a HedFileError exception and translates it to human readable
 
         Parameters
         ----------
@@ -28,7 +31,7 @@ def format_error_message(self, include_tabbing=True, return_string_only=False,
         error_list: [{}]
             A list(of one) error formatted into a human readable dictionary.
         """
-        error_prefix = self.SCHEMA_ERROR_PREFIX
+        error_prefix = "ERROR: "
         if include_tabbing:
             error_prefix = "\t" + error_prefix
 
@@ -36,9 +39,11 @@ def format_error_message(self, include_tabbing=True, return_string_only=False,
         if display_filename:
             filename = display_filename
         error_types = {
-            SchemaErrors.FILE_NOT_FOUND: f"{error_prefix}{message}.  '{filename}'",
-            SchemaErrors.CANNOT_PARSE_XML: f"{error_prefix}Cannot parse schema XML file: "
-                                           f"{message}.  '{filename}'",
+            HedExceptions.FILE_NOT_FOUND: f"{error_prefix}{message}.  '{filename}'",
+            HedExceptions.INVALID_EXTENSION: f"{error_prefix}Invalid extension.  '{filename}'",
+            HedExceptions.CANNOT_PARSE_XML: f"{error_prefix}Cannot parse schema XML: "
+                                            f"{message}.  '{filename}'",
+            HedExceptions.CANNOT_PARSE_JSON: f"{error_prefix}Cannot parse json: {message}. '{filename}'",
         }
         default_error_message = f'{error_prefix}Internal Error'
         error_message = error_types.get(error_type, default_error_message)
diff --git a/hedtools/hed/util/file_util.py b/hedtools/hed/util/file_util.py
index 9304ab060..1a561b86e 100644
--- a/hedtools/hed/util/file_util.py
+++ b/hedtools/hed/util/file_util.py
@@ -9,7 +9,7 @@
 NO_VERSION_INFO_STRING = "No version info found"
 
 
-def delete_file_if_it_exist(file_path):
+def delete_file_if_it_exists(file_path):
     """Deletes a other if it exist.
 
     Parameters
@@ -22,6 +22,9 @@ def delete_file_if_it_exist(file_path):
     bool
         True if the file exists and was deleted.
     """
+    if file_path is None:
+        return False
+
     if os.path.isfile(file_path):
         os.remove(file_path)
         return True
diff --git a/hedtools/hed/util/hed_schema.py b/hedtools/hed/util/hed_schema.py
index d0c328bc4..f3914a6d7 100644
--- a/hedtools/hed/util/hed_schema.py
+++ b/hedtools/hed/util/hed_schema.py
@@ -9,8 +9,7 @@
 
 from defusedxml.ElementTree import parse
 import xml
-from hed.util.exceptions import SchemaFileError
-from hed.util.error_types import SchemaErrors
+from hed.util.exceptions import HedFileError, HedExceptions
 
 
 # These need to match the attributes/element name/etc used to load from the xml
@@ -67,7 +66,6 @@ def __init__(self, hed_xml_file_path):
         ----------
         hed_xml_file_path: str
             The path to a HED XML file.
-
         Returns
         -------
         HedSchema
@@ -469,9 +467,11 @@ def parse_hed_xml_file(hed_xml_file_path):
         try:
             hed_xml_tree = parse(hed_xml_file_path)
         except xml.etree.ElementTree.ParseError as e:
-            raise SchemaFileError(SchemaErrors.CANNOT_PARSE_XML, e.msg, hed_xml_file_path)
+            raise HedFileError(HedExceptions.CANNOT_PARSE_XML, e.msg, hed_xml_file_path)
         except FileNotFoundError as e:
-            raise SchemaFileError(SchemaErrors.FILE_NOT_FOUND, e.strerror, hed_xml_file_path)
+            raise HedFileError(HedExceptions.FILE_NOT_FOUND, e.strerror, hed_xml_file_path)
+        except TypeError as e:
+            raise HedFileError(HedExceptions.FILE_NOT_FOUND, str(e), hed_xml_file_path)
         return hed_xml_tree.getroot()
 
     def _get_ancestor_tag_names(self, tag_element):
diff --git a/hedtools/hed/validator/hed_validator.py b/hedtools/hed/validator/hed_validator.py
index d3d2eb752..5a608b3af 100644
--- a/hedtools/hed/validator/hed_validator.py
+++ b/hedtools/hed/validator/hed_validator.py
@@ -4,71 +4,100 @@
 the get_validation_issues() function.
 
 """
-from hed.util.error_types import ValidationErrors, ErrorContext
+from hed.util.error_types import ErrorContext
 from hed.util import hed_cache
 from hed.util import error_reporter
-from hed.util.error_reporter import push_error_context, pop_error_context
 from hed.util.hed_schema import HedSchema
 from hed.util.hed_string_delimiter import HedStringDelimiter
 from hed.validator.tag_validator import TagValidator
 from hed.util.hed_file_input import BaseFileInput
-from hed.util.exceptions import SchemaFileError
+from hed.util.exceptions import HedFileError, HedExceptions
 
 
 class HedValidator:
-    def __init__(self, hed_input, check_for_warnings=False, run_semantic_validation=True,
+    def __init__(self, check_for_warnings=False, run_semantic_validation=True,
                  hed_xml_file='', xml_version_number=None,
-                 hed_schema=None):
+                 hed_schema=None, error_handler=None):
         """Constructor for the HedValidator class.
 
         Parameters
         ----------
-        hed_input: str or list or HedFileInput object
-            A list of HED strings, a single HED string, or a HedFileInput object.
-            If it is a single string or a list, validate them as hed strings.
         check_for_warnings: bool
             True if the validator should check for warnings. False if the validator should only report errors.
         run_semantic_validation: bool
             True if the validator should check the HED data against a schema. False for syntax-only validation.
         hed_xml_file: str
             A path to a specific hed xml file, or a directory containing a hed xml file.
+            Note: An invalid schema here will throw a HedFileError exception.  Use hed_schema to pass an already
+            created schema.
         xml_version_number: str
             HED version format string. Expected format: 'X.Y.Z'  Only applies if hed_xml_file is empty,
                 or does not point to a specific xml file.
         hed_schema: HedSchema
             Name of already prepared HedSchema to use.  This overrides hed_xml_file and xml_version_number.
+        error_handler : ErrorHandler or None
+            Used to report errors.  Uses a default one if none passed in.
         Returns
         -------
         HedValidator object
             A HedValidator object.
 
         """
-        self._is_file = isinstance(hed_input, BaseFileInput)
-        self._hed_input = hed_input
-        self._validation_issues = []
         self._tag_validator = None
+        if error_handler is None:
+            error_handler = error_reporter.ErrorHandler()
+        self._error_handler = error_handler
         if run_semantic_validation:
-            try:
-                if hed_schema is None:
-                    self._hed_schema = self._get_hed_schema(hed_xml_file,
-                                                            get_specific_version=xml_version_number)
-                else:
-                    self._hed_schema = hed_schema
-                self._tag_validator = TagValidator(hed_schema=self._hed_schema,
-                                                   check_for_warnings=check_for_warnings,
-                                                   run_semantic_validation=True)
-            except SchemaFileError as e:
-                self._hed_schema = None
-                self._validation_issues += e.format_error_message()
+            if hed_schema is None:
+                self._hed_schema = self._get_hed_schema(hed_xml_file,
+                                                        get_specific_version=xml_version_number)
+            else:
+                self._hed_schema = hed_schema
+            self._tag_validator = TagValidator(hed_schema=self._hed_schema,
+                                               check_for_warnings=check_for_warnings,
+                                               run_semantic_validation=True,
+                                               error_handler=self._error_handler)
 
         # Fall back to syntax validation if we don't have a tag validator at this point
         if self._tag_validator is None:
             self._tag_validator = TagValidator(check_for_warnings=check_for_warnings,
-                                               run_semantic_validation=False)
+                                               run_semantic_validation=False,
+                                               error_handler=self._error_handler)
 
-        self._validation_issues += self._validate_hed_input()
         self._run_semantic_validation = run_semantic_validation
 
+    def validate_input(self, hed_input, display_filename=None):
+        """
+            Validates any given hed_input string, file, or list and returns a list of issues.
+
+        Parameters
+        ----------
+        hed_input: str or list or HedFileInput object
+            A list of HED strings, a single HED string, or a HedFileInput object.
+            If it is a single string or a list, validate them as hed strings.
+        display_filename: str
+            If present, will use this as the filename for context, rather than using the actual filename
+            Useful for temp filenames.
+        Returns
+        -------
+        validation_issues : [{}]
+        """
+        is_file = isinstance(hed_input, BaseFileInput)
+        if not display_filename and is_file:
+            display_filename = hed_input.filename
+        if isinstance(hed_input, list):
+            validation_issues = self._validate_hed_strings(hed_input)
+        elif is_file:
+            self._error_handler.push_error_context(ErrorContext.FILE_NAME, display_filename)
+            validation_issues = self._validate_hed_tags_in_file(hed_input)
+        else:
+            validation_issues = self._validate_hed_strings([hed_input])[0]
+
+        if is_file:
+            # If we have a custom title and found no issues, we need to still print the title.
+            self._error_handler.pop_error_context()
+        return validation_issues
+
     def get_tag_validator(self):
         """Gets a TagValidator object.
 
@@ -109,50 +138,23 @@ def _get_hed_schema(hed_xml_file, get_specific_version=None):
         hed_schema = HedSchema(final_hed_xml_file)
         return hed_schema
 
-    def _validate_hed_input(self):
-        """Validates the HED tags in a string or a file.
-
-         Parameters
-         ----------
-         Returns
-         -------
-         list
-             The issues that were found.
+    def _validate_hed_tags_in_file(self, hed_input):
         """
-        if isinstance(self._hed_input, list):
-            validation_issues = self._validate_hed_strings(self._hed_input)
-        elif self._is_file:
-            if self._hed_input and self._hed_input.is_valid_extension():
-                validation_issues = self._validate_hed_tags_in_file()
-            else:
-                validation_issues = error_reporter.format_val_error(ValidationErrors.INVALID_FILENAME,
-                                                                    file_name=self._hed_input.filename)
-        else:
-            validation_issues = self._validate_hed_strings([self._hed_input])[0]
-        return validation_issues
 
-    def _validate_hed_tags_in_file(self):
+        Parameters
+        ----------
+        hed_input: HedFileInput object
+            A file to validate.  This function does no type checking on this.
+        Returns
+        -------
+        validation_issues : [{}]
+        """
         validation_issues = []
-
-        for row_number, row_hed_string, column_to_hed_tags_dictionary in self._hed_input:
+        for row_number, row_hed_string, column_to_hed_tags_dictionary in hed_input:
             validation_issues = self._append_validation_issues_if_found(validation_issues, row_number, row_hed_string,
                                                                         column_to_hed_tags_dictionary)
-
         return validation_issues
 
-    def get_validation_issues(self):
-        """Gets the issues.
-
-         Parameters
-         ----------
-         Returns
-         -------
-         list
-             The issues that were found.
-
-         """
-        return self._validation_issues
-
     def _append_validation_issues_if_found(self, validation_issues, row_number, row_hed_string,
                                            column_to_hed_tags_dictionary):
         """Appends the issues associated with a particular row and/or column in a spreadsheet.
@@ -197,12 +199,12 @@ def _append_row_validation_issues_if_found(self, validation_issues, row_number,
 
          """
         if row_hed_string:
-            push_error_context(ErrorContext.ROW, row_number)
+            self._error_handler.push_error_context(ErrorContext.ROW, row_number)
             hed_string_delimiter = HedStringDelimiter(row_hed_string)
             row_validation_issues = self._validate_top_level_in_hed_string(hed_string_delimiter)
             row_validation_issues += self._validate_tag_levels_in_hed_string(hed_string_delimiter)
             validation_issues += row_validation_issues
-            pop_error_context()
+            self._error_handler.pop_error_context()
         return validation_issues
 
     def _append_column_validation_issues_if_found(self, validation_issues, row_number, column_to_hed_tags_dictionary):
@@ -223,11 +225,13 @@ def _append_column_validation_issues_if_found(self, validation_issues, row_numbe
 
          """
         if column_to_hed_tags_dictionary:
+            self._error_handler.push_error_context(ErrorContext.ROW, row_number)
             for column_number in column_to_hed_tags_dictionary.keys():
-                push_error_context(ErrorContext.COLUMN, row_number, column_context=column_number)
+                self._error_handler.push_error_context(ErrorContext.COLUMN, column_number)
                 column_hed_string = column_to_hed_tags_dictionary[column_number]
                 validation_issues += self.validate_column_hed_string(column_hed_string)
-                pop_error_context()
+                self._error_handler.pop_error_context()
+            self._error_handler.pop_error_context()
         return validation_issues
 
     def validate_column_hed_string(self, column_hed_string):
@@ -367,30 +371,27 @@ def _validate_individual_tags_in_hed_string(self, hed_string_delimiter):
                                                                   previous_formatted_tag=previous_formatted_tag)
         return validation_issues
 
-    def get_printable_issue_string(self, title=''):
-        """Return a string with identifying title and issues in string form one per line.
-
-          Parameters
-          ----------
-         title: str
-             String used as a title for the issues.
-
-          Returns
-          -------
-          str
-              A str containing printable version of the issues or '[]'.
+    @staticmethod
+    def get_printable_issue_string(validation_issues, title=None, severity=None, skip_filename=True):
+        """Return a string with issues list flatted into single string, one per line
 
-          """
+        Parameters
+        ----------
+        validation_issues: []
+            Issues to print
+        title: str
+            Optional title that will always show up first if present(even if there are no validation issues)
+        severity: int
+            Return only warnings >= severity
+        skip_filename: bool
+            If true, don't add the filename context to the printable string.
+        Returns
+        -------
+        str
+            A str containing printable version of the issues or '[]'.
 
-        if not self._validation_issues:
-            issue_string = "\t[]\n"
-        else:
-            issue_string = "\n"
-            for el in self._validation_issues:
-                issue_string = issue_string + "\t" + el["code"] + el["message"] + "\n"
-        if title:
-            issue_string = title + ":" + issue_string
-        return issue_string
+        """
+        return error_reporter.ErrorHandler.get_printable_issue_string(validation_issues, title, severity, skip_filename)
 
     @staticmethod
     def get_previous_original_and_formatted_tag(original_and_formatted_tags, loop_index):
diff --git a/hedtools/hed/validator/tag_validator.py b/hedtools/hed/validator/tag_validator.py
index 11b9df88e..d2d854e03 100644
--- a/hedtools/hed/validator/tag_validator.py
+++ b/hedtools/hed/validator/tag_validator.py
@@ -30,7 +30,7 @@ class TagValidator:
     TIME_UNIT_CLASS = 'time'
 
     def __init__(self, hed_schema=None, check_for_warnings=False, run_semantic_validation=True,
-                 allow_numbers_to_be_pound_sign=False):
+                 allow_numbers_to_be_pound_sign=False, error_handler=None):
         """Constructor for the Tag_Validator class.
 
         Parameters
@@ -39,21 +39,23 @@ def __init__(self, hed_schema=None, check_for_warnings=False, run_semantic_valid
             A HedSchema object.
         allow_numbers_to_be_pound_sign: bool
             If true, considers # equal to a number for validation purposes.  This is so it can validate templates.
+        error_handler : ErrorHandler or None
+            Used to report errors.  Uses a default one if none passed in.
         Returns
         -------
         TagValidator
             A Tag_Validator object.
 
         """
+        if error_handler is None:
+            error_handler = error_reporter.ErrorHandler()
+        self._error_handler = error_handler
         self._hed_schema = hed_schema
         if hed_schema:
             self._hed_schema_dictionaries = hed_schema.get_dictionaries()
         else:
             self._hed_schema_dictionaries = None
         self._check_for_warnings = check_for_warnings
-        self._issue_count = 0
-        self._error_count = 0
-        self._warning_count = 0
         self._run_semantic_validation = run_semantic_validation
 
         if allow_numbers_to_be_pound_sign:
@@ -61,62 +63,6 @@ def __init__(self, hed_schema=None, check_for_warnings=False, run_semantic_valid
         else:
             self._digit_expression = self.DIGIT_EXPRESSION
 
-    def _increment_issue_count(self, is_error=True):
-        """Increments the validation issue count
-
-         Parameters
-         ----------
-         is_error: bool
-            True if the issue is an error, False if it is not.
-         Returns
-         -------
-
-         """
-        self._issue_count += 1
-        if is_error:
-            self._error_count += 1
-        else:
-            self._warning_count += 1
-
-    def get_issue_count(self):
-        """Gets the issue count
-
-         Parameters
-         ----------
-
-         Returns
-         -------
-         int
-            The issue count
-         """
-        return self._issue_count
-
-    def get_warning_count(self):
-        """Gets the warning count
-
-         Parameters
-         ----------
-
-         Returns
-         -------
-         int
-            The warning count
-         """
-        return self._warning_count
-
-    def get_error_count(self):
-        """Gets the error count
-
-         Parameters
-         ----------
-
-         Returns
-         -------
-         int
-            The error count
-         """
-        return self._error_count
-
     def run_individual_tag_validators(self, original_tag, formatted_tag, previous_original_tag='',
                                       previous_formatted_tag=''):
         """Runs the validators on the individual tags in a HED string.
@@ -254,13 +200,11 @@ def tag_exist_in_schema(self, original_tag, formatted_tag, previous_original_tag
             return validation_issues
 
         if not is_extension_tag and self.tag_takes_value(previous_formatted_tag):
-            validation_issues += error_reporter.format_val_error(ValidationErrors.INVALID_COMMA,
-                                                                 tag=original_tag,
-                                                                 previous_tag=previous_original_tag)
-            self._increment_issue_count()
+            validation_issues += self._error_handler.format_val_error(ValidationErrors.INVALID_COMMA,
+                                                                      tag=original_tag,
+                                                                      previous_tag=previous_original_tag)
         elif not is_extension_tag:
-            validation_issues += error_reporter.format_val_error(ValidationErrors.INVALID_TAG, tag=original_tag)
-            self._increment_issue_count()
+            validation_issues += self._error_handler.format_val_error(ValidationErrors.INVALID_TAG, tag=original_tag)
         return validation_issues
 
     def tag_exists_in_schema(self, formatted_tag):
@@ -300,9 +244,8 @@ def check_capitalization(self, original_tag, formatted_tag):
         for tag_name in tag_names:
             correct_tag_name = tag_name.capitalize()
             if tag_name != correct_tag_name and not re.search(self.CAMEL_CASE_EXPRESSION, tag_name):
-                validation_issues += error_reporter.format_val_warning(ValidationWarnings.CAPITALIZATION,
-                                                                       tag=original_tag)
-                self._increment_issue_count(is_error=False)
+                validation_issues += self._error_handler.format_val_warning(ValidationWarnings.CAPITALIZATION,
+                                                                            tag=original_tag)
                 break
         return validation_issues
 
@@ -427,11 +370,10 @@ def check_if_tag_unit_class_units_are_valid(self, original_tag, formatted_tag):
                                              tag_unit_class_units)):
                 pass
             else:
-                validation_issues += error_reporter.format_val_error(ValidationErrors.UNIT_CLASS_INVALID_UNIT,
-                                                                     tag=original_tag,
-                                                                     unit_class_units=','.join(
-                                                                         sorted(tag_unit_class_units)))
-                self._increment_issue_count()
+                validation_issues += self._error_handler.format_val_error(ValidationErrors.UNIT_CLASS_INVALID_UNIT,
+                                                                          tag=original_tag,
+                                                                          unit_class_units=','.join(
+                                                                              sorted(tag_unit_class_units)))
         return validation_issues
 
     def get_valid_unit_plural(self, unit):
@@ -546,10 +488,9 @@ def check_if_tag_unit_class_units_exist(self, original_tag, formatted_tag):
             tag_unit_values = self.get_tag_name(formatted_tag)
             if re.search(self._digit_expression, tag_unit_values):
                 default_unit = self.get_unit_class_default_unit(formatted_tag)
-                validation_issues += error_reporter.format_val_warning(ValidationWarnings.UNIT_CLASS_DEFAULT_USED,
-                                                                       tag=original_tag,
-                                                                       default_unit=default_unit)
-                self._increment_issue_count(is_error=False)
+                validation_issues += self._error_handler.format_val_warning(ValidationWarnings.UNIT_CLASS_DEFAULT_USED,
+                                                                            tag=original_tag,
+                                                                            default_unit=default_unit)
         return validation_issues
 
     def get_tag_name(self, tag):
@@ -665,8 +606,8 @@ def check_number_of_group_tildes(self, tag_group, tag_group_string):
         """
         validation_issues = []
         if tag_group.count('~') > 2:
-            validation_issues += error_reporter.format_val_error(ValidationErrors.EXTRA_TILDE, tag=tag_group_string)
-            self._increment_issue_count()
+            validation_issues += self._error_handler.format_val_error(ValidationErrors.EXTRA_TILDE,
+                                                                      tag=tag_group_string)
         return validation_issues
 
     def check_if_tag_requires_child(self, original_tag, formatted_tag):
@@ -686,9 +627,8 @@ def check_if_tag_requires_child(self, original_tag, formatted_tag):
         """
         validation_issues = []
         if self._hed_schema_dictionaries[HedKey.RequireChild].get(formatted_tag):
-            validation_issues += error_reporter.format_val_error(ValidationErrors.REQUIRE_CHILD,
-                                                                 tag=original_tag)
-            self._increment_issue_count()
+            validation_issues += self._error_handler.format_val_error(ValidationErrors.REQUIRE_CHILD,
+                                                                      tag=original_tag)
         return validation_issues
 
     def check_for_required_tags(self, formatted_top_level_tags):
@@ -710,10 +650,9 @@ def check_for_required_tags(self, formatted_top_level_tags):
             capitalized_required_tag_prefix = \
                 self._hed_schema_dictionaries[HedKey.RequiredPrefix][required_tag_prefix]
             if sum([x.startswith(required_tag_prefix) for x in formatted_top_level_tags]) < 1:
-                validation_issues += error_reporter.format_val_warning(
+                validation_issues += self._error_handler.format_val_warning(
                     ValidationWarnings.REQUIRED_PREFIX_MISSING,
                     tag_prefix=capitalized_required_tag_prefix)
-                self._increment_issue_count(is_error=False)
         return validation_issues
 
     def check_if_multiple_unique_tags_exist(self, original_tag_list, formatted_tag_list):
@@ -736,10 +675,9 @@ def check_if_multiple_unique_tags_exist(self, original_tag_list, formatted_tag_l
         for unique_tag_prefix in unique_tag_prefixes:
             unique_tag_prefix_bool_mask = [x.startswith(unique_tag_prefix) for x in formatted_tag_list]
             if sum(unique_tag_prefix_bool_mask) > 1:
-                validation_issues += error_reporter.format_val_error(
+                validation_issues += self._error_handler.format_val_error(
                     ValidationErrors.MULTIPLE_UNIQUE,
                     tag_prefix=self._hed_schema_dictionaries[HedKey.Unique][unique_tag_prefix])
-                self._increment_issue_count()
         return validation_issues
 
     def tag_has_unique_prefix(self, tag):
@@ -788,9 +726,8 @@ def check_if_duplicate_tags_exist(self, original_tag_list, formatted_tag_list):
                         tag_index not in duplicate_indices and duplicate_index not in duplicate_indices:
                     duplicate_indices.add(tag_index)
                     duplicate_indices.add(duplicate_index)
-                    validation_issues += error_reporter.format_val_error(ValidationErrors.DUPLICATE,
-                                                                         tag=original_tag_list[tag_index])
-                    self._increment_issue_count()
+                    validation_issues += self._error_handler.format_val_error(ValidationErrors.DUPLICATE,
+                                                                              tag=original_tag_list[tag_index])
         return validation_issues
 
     def get_tag_slash_indices(self, tag, slash='/'):
@@ -854,10 +791,10 @@ def find_delimiter_issues_in_hed_string(self, hed_string):
                 continue
             if TagValidator.character_is_delimiter(current_character):
                 if current_tag.strip() == current_character:
-                    issues += error_reporter.format_val_error(ValidationErrors.EXTRA_DELIMITER,
-                                                              character=current_character,
-                                                              index=i,
-                                                              hed_string=hed_string)
+                    issues += self._error_handler.format_val_error(ValidationErrors.EXTRA_DELIMITER,
+                                                                   character=current_character,
+                                                                   index=i,
+                                                                   hed_string=hed_string)
                     current_tag = ''
                     continue
                 current_tag = ''
@@ -865,18 +802,18 @@ def find_delimiter_issues_in_hed_string(self, hed_string):
                 if current_tag.strip() == self.OPENING_GROUP_CHARACTER:
                     current_tag = ''
                 else:
-                    issues += error_reporter.format_val_error(ValidationErrors.INVALID_TAG, tag=current_tag)
+                    issues += self._error_handler.format_val_error(ValidationErrors.INVALID_TAG, tag=current_tag)
             elif TagValidator.comma_is_missing_after_closing_parentheses(last_non_empty_valid_character,
                                                                          current_character):
-                issues += error_reporter.format_val_error(ValidationErrors.COMMA_MISSING, tag=current_tag[:-1])
+                issues += self._error_handler.format_val_error(ValidationErrors.COMMA_MISSING, tag=current_tag[:-1])
                 break
             last_non_empty_valid_character = current_character
             last_non_empty_valid_index = i
         if TagValidator.character_is_delimiter(last_non_empty_valid_character):
-            issues += error_reporter.format_val_error(ValidationErrors.EXTRA_DELIMITER,
-                                                      character=last_non_empty_valid_character,
-                                                      index=last_non_empty_valid_index,
-                                                      hed_string=hed_string)
+            issues += self._error_handler.format_val_error(ValidationErrors.EXTRA_DELIMITER,
+                                                           character=last_non_empty_valid_character,
+                                                           index=last_non_empty_valid_index,
+                                                           hed_string=hed_string)
         return issues
 
     def skip_iterations(self, iterator, start, end):
@@ -901,8 +838,7 @@ def skip_iterations(self, iterator, start, end):
             next(iterator, None)
         return iterator
 
-    @staticmethod
-    def report_invalid_character_error(character, index, hed_string):
+    def report_invalid_character_error(self, character, index, hed_string):
         """Reports a error that is related to an invalid character.
 
         Parameters
@@ -919,8 +855,9 @@ def report_invalid_character_error(character, index, hed_string):
             A singleton list with a dictionary representing the error.
 
         """
-        return error_reporter.format_val_error(ValidationErrors.INVALID_CHARACTER, character=character, index=index,
-                                               hed_string=hed_string)
+        return self._error_handler.format_val_error(ValidationErrors.INVALID_CHARACTER, character=character,
+                                                    index=index,
+                                                    hed_string=hed_string)
 
     @staticmethod
     def comma_is_missing_after_closing_parentheses(last_non_empty_character, current_character):
@@ -942,8 +879,8 @@ def comma_is_missing_after_closing_parentheses(last_non_empty_character, current
 
         """
         return last_non_empty_character == TagValidator.CLOSING_GROUP_CHARACTER and \
-            not (TagValidator.character_is_delimiter(current_character) or
-                 current_character == TagValidator.CLOSING_GROUP_CHARACTER)
+               not (TagValidator.character_is_delimiter(current_character) or
+                    current_character == TagValidator.CLOSING_GROUP_CHARACTER)
 
     @staticmethod
     def character_is_delimiter(character):
@@ -977,8 +914,7 @@ def find_invalid_character_issues(self, hed_string):
         validation_issues = []
         for index, character in enumerate(hed_string):
             if character in TagValidator.INVALID_CHARS:
-                validation_issues += TagValidator.report_invalid_character_error(character, index, hed_string)
-                self._increment_issue_count()
+                validation_issues += self.report_invalid_character_error(character, index, hed_string)
 
         return validation_issues
 
@@ -1000,11 +936,10 @@ def count_tag_group_parentheses(self, hed_string):
         number_of_opening_parentheses = hed_string.count('(')
         number_of_closing_parentheses = hed_string.count(')')
         if number_of_opening_parentheses != number_of_closing_parentheses:
-            validation_issues += error_reporter.format_val_error(
+            validation_issues += self._error_handler.format_val_error(
                 ValidationErrors.PARENTHESES,
                 opening_parentheses_count=number_of_opening_parentheses,
                 closing_parentheses_count=number_of_closing_parentheses)
-            self._increment_issue_count()
         return validation_issues
 
     @staticmethod
diff --git a/hedtools/requirements.txt b/hedtools/requirements.txt
index a4cc38bcc..3ed7e91f6 100644
--- a/hedtools/requirements.txt
+++ b/hedtools/requirements.txt
@@ -7,7 +7,7 @@ openpyxl==3.0.5
 pandas==1.1.4
 portalocker==2.0.0
 python-dateutil==2.8.1
-pytz==2020.4
+pytz==2021.1
 semantic_version==2.8.5
 six==1.15.0
 xlrd==1.2.0
diff --git a/hedweb/hed/web/constants/common_constants.py b/hedweb/hed/web/constants/common_constants.py
index ce9b1dcaf..894437100 100644
--- a/hedweb/hed/web/constants/common_constants.py
+++ b/hedweb/hed/web/constants/common_constants.py
@@ -12,6 +12,9 @@
 JSON_PATH = 'json-path'
 ERROR_COUNT = 'error-count'
 
+EVENTS_VALIDATION_SUBMIT = 'events-validation-submit'
+EVENTS_SUBMIT_FLASH = 'events-validation-submit-flash'
+
 HED_DISPLAY_NAME = 'hed-display-name'
 HED_MAJOR_VERSIONS = 'hed-major-versions'
 HED_OTHER_VERSION = 'hed-other-version'
@@ -51,6 +54,9 @@
 SCHEMA_URL_FLASH = 'schema-url-flash'
 SCHEMA_URL_DISPLAY_NAME = 'schema-url-display-name'
 SCHEMA_URL_OPTION = 'schema-url-option'
+
+SERVICE_SUBMIT = 'service-submit'
+
 # Type constants
 BOOLEAN = 'boolean'
 STRING = 'string'
@@ -58,7 +64,7 @@
 
 # Validation-specific constants
 VALIDATION_FORM = 'validation-form'
-VALIDATION_OUTPUT_FILE_PREFIX = 'validated_'
+VALIDATION_OUTPUT_FILE_PREFIX = 'validation_errors'
 VALIDATION_SUBMIT = 'validation-submit'
 VALIDATION_SUBMIT_FLASH = 'validation-submit-flash'
-EEG_SUBMIT = 'eeg-submit'
+
diff --git a/hedweb/hed/web/constants/page_constants.py b/hedweb/hed/web/constants/page_constants.py
index 982fe8f00..77129f215 100644
--- a/hedweb/hed/web/constants/page_constants.py
+++ b/hedweb/hed/web/constants/page_constants.py
@@ -1,8 +1,9 @@
 ADDITIONAL_EXAMPLES_PAGE = 'additional-examples.html'
 COMMON_ERRORS_PAGE = 'common-errors.html'
 DICTIONARY_VALIDATION_PAGE = 'dictionary.html'
-EEG_VALIDATION_PAGE = 'eeg-spreadsheet.html'
+EVENTS_PAGE = 'events.html'
+HED_SERVICES_PAGE = 'hed-services.html'
 HED_TOOLS_HELP_PAGE = 'hed-tools-help.html'
 HED_TOOLS_HOME_PAGE = 'hed-tools-home.html'
 SCHEMA_PAGE = 'schema.html'
-SPREADSHEET_VALIDATION_PAGE = 'spreadsheet.html'
+SPREADSHEET_PAGE = 'spreadsheet.html'
diff --git a/hedweb/hed/web/constants/route_constants.py b/hedweb/hed/web/constants/route_constants.py
index 824f43c16..5883c945f 100644
--- a/hedweb/hed/web/constants/route_constants.py
+++ b/hedweb/hed/web/constants/route_constants.py
@@ -1,19 +1,22 @@
 ADDITIONAL_EXAMPLES_ROUTE = '/additional-examples'
 COMMON_ERRORS_ROUTE = '/common-errors'
 DELETE_FILE_ROUTE = '/delete/<filename>'
-DICTIONARY_VALIDATION_ROUTE = '/dictionary-validation'
+DICTIONARY_ROUTE = '/dictionary'
 DICTIONARY_VALIDATION_SUBMIT_ROUTE = '/dictionary-validation-submit'
 DOWNLOAD_FILE_ROUTE = '/download-file/<filename>'
-EEG_VALIDATION_SUBMIT_ROUTE = '/eeg-validation-submit'
-EEG_VALIDATION_ROUTE = '/eeg-validation'
+
+EVENTS_ROUTE = '/events'
+EVENTS_VALIDATION_SUBMIT_ROUTE = '/events-validation-submit'
 HED_MAJOR_VERSION_ROUTE = '/get-hed-major-versions'
+HED_SERVICES_SUBMIT_ROUTE = '/hed-services-submit'
+HED_SERVICES_ROUTE = '/hed-services'
 HED_TOOLS_HELP_ROUTE = '/hed-tools-help'
 HED_TOOLS_HOME_ROUTE = '/'
 HED_VERSION_ROUTE = '/get-hed-version'
 SCHEMA_ROUTE = '/schema'
-SCHEMA_CONVERSION_SUBMIT_ROUTE = '/schema-conversion-submit'
 SCHEMA_COMPLIANCE_CHECK_SUBMIT_ROUTE = '/schema-compliance-check-submit'
+SCHEMA_CONVERSION_SUBMIT_ROUTE = '/schema-conversion-submit'
 SPREADSHEET_COLUMN_INFO_ROUTE = '/get-spreadsheet-columns-info'
-SPREADSHEET_VALIDATION_ROUTE = '/spreadsheet-validation'
+SPREADSHEET_ROUTE = '/spreadsheet'
 SPREADSHEET_VALIDATION_SUBMIT_ROUTE = '/spreadsheet-validation-submit'
 WORKSHEET_COLUMN_INFO_ROUTE = '/get-worksheets-info'
diff --git a/hedweb/hed/web/dictionary.py b/hedweb/hed/web/dictionary.py
index 45ff7f6b9..57caece50 100644
--- a/hedweb/hed/web/dictionary.py
+++ b/hedweb/hed/web/dictionary.py
@@ -1,28 +1,27 @@
 import json
-import os
 import traceback
 from urllib.error import URLError, HTTPError
 from flask import current_app
-from werkzeug.utils import secure_filename
 
-from hed.util.file_util import get_file_extension, delete_file_if_it_exist
+from hed.util.file_util import get_file_extension, delete_file_if_it_exists, write_strings_to_file
 from hed.validator.hed_validator import HedValidator
 from hed.util.column_def_group import ColumnDefGroup
 from hed.util.hed_schema import HedSchema
 
 from hed.web.constants import common_constants, error_constants, file_constants
-from hed.web import web_utils
-from hed.web import utils
+from hed.web.web_utils import generate_filename, generate_download_file_response, \
+    handle_http_error, get_hed_path_from_pull_down, get_printable_issue_string, \
+    get_uploaded_file_path_from_form, save_file_to_upload_folder, save_text_to_upload_folder, get_optional_form_field
 
 app_config = current_app.config
 
 
-def generate_arguments_from_validation_form(form_request_object):
+def generate_arguments_from_dictionary_form(request):
     """Gets the validation function input arguments from a request object associated with the validation form.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from the validation form.
 
     Returns
@@ -30,36 +29,20 @@ def generate_arguments_from_validation_form(form_request_object):
     dictionary
         A dictionary containing input arguments for calling the underlying validation function.
     """
-    hed_file_path, hed_display_name = web_utils.get_hed_path_from_pull_down(form_request_object)
-    uploaded_file_name, original_file_name = \
-        web_utils.get_uploaded_file_path_from_form(form_request_object, common_constants.JSON_FILE,
-                                                   file_constants.DICTIONARY_FILE_EXTENSIONS)
+    hed_file_path, hed_display_name = get_hed_path_from_pull_down(request)
+    uploaded_file_name, original_file_name = get_uploaded_file_path_from_form(request, common_constants.JSON_FILE,
+                                                                              file_constants.DICTIONARY_FILE_EXTENSIONS)
 
     input_arguments = {common_constants.HED_XML_FILE: hed_file_path,
                        common_constants.HED_DISPLAY_NAME: hed_display_name,
                        common_constants.JSON_PATH: uploaded_file_name,
-                       common_constants.JSON_FILE: original_file_name}
-    input_arguments[common_constants.CHECK_FOR_WARNINGS] = utils.get_optional_form_field(
-        form_request_object, common_constants.CHECK_FOR_WARNINGS, common_constants.BOOLEAN)
-    return input_arguments
-
-
-def generate_dictionary_validation_filename(dictionary_filename):
-    """Generates a filename for the attachment that will contain the dictionary validation issues.
-
-    Parameters
-    ----------
-    dictionary_filename: string
-        The name of the dictionary file.
+                       common_constants.JSON_FILE: original_file_name,
+                       common_constants.CHECK_FOR_WARNINGS: get_optional_form_field(request,
+                                                                                    common_constants.CHECK_FOR_WARNINGS,
+                                                                                    common_constants.BOOLEAN)
+                       }
 
-    Returns
-    -------
-    string
-        The name of the attachment other containing the validation issues.
-    """
-
-    return common_constants.VALIDATION_OUTPUT_FILE_PREFIX + secure_filename(dictionary_filename).rsplit('.')[
-        0] + file_constants.TEXT_EXTENSION
+    return input_arguments
 
 
 def report_eeg_events_validation_status(request):
@@ -85,7 +68,7 @@ def report_eeg_events_validation_status(request):
     check_for_warnings = form_data["check_for_warnings"] == '1' if "check_for_warnings" in form_data else False
     # if hed_xml_file was submitted, it's accessed by request.files, otherwise empty
     if "hed-xml-file" in request.files and get_file_extension(request.files["hed-xml-file"].filename) == "xml":
-        hed_xml_file = web_utils.save_file_to_upload_folder(request.files["hed-xml-file"])
+        hed_xml_file = save_file_to_upload_folder(request.files["hed-xml-file"])
     else:
         hed_xml_file = ''
 
@@ -103,17 +86,17 @@ def report_eeg_events_validation_status(request):
     except:
         validation_status[error_constants.ERROR_KEY] = traceback.format_exc()
     finally:
-        delete_file_if_it_exist(hed_xml_file)
+        delete_file_if_it_exists(hed_xml_file)
 
     return validation_status
 
 
-def report_dictionary_validation_status(form_request_object):
+def report_dictionary_validation_status(request):
     """Reports the spreadsheet validation status.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from the validation form.
 
     Returns
@@ -124,16 +107,8 @@ def report_dictionary_validation_status(form_request_object):
     """
     input_arguments = []
     try:
-        input_arguments = generate_arguments_from_validation_form(form_request_object)
-        def_group = validate_dictionary(input_arguments)
-        validation_issues = def_group.get_validation_issues()
-        if validation_issues:
-            issue_file = save_issues_to_upload_folder(input_arguments[common_constants.JSON_FILE],
-                                                      validation_issues)
-            download_response = web_utils.generate_download_file_response(issue_file)
-            if isinstance(download_response, str):
-                return web_utils.handle_http_error(error_constants.NOT_FOUND_ERROR, download_response)
-            return download_response
+        input_arguments = generate_arguments_from_dictionary_form(request)
+        return validate_dictionary(input_arguments, hed_schema=None)
     except HTTPError:
         return error_constants.NO_URL_CONNECTION_ERROR
     except URLError:
@@ -141,53 +116,42 @@ def report_dictionary_validation_status(form_request_object):
     except Exception as e:
         return "Unexpected processing error: " + str(e)
     finally:
-        delete_file_if_it_exist(input_arguments[common_constants.JSON_PATH])
-        # delete_file_if_it_exist(input_arguments[common_constants.HED_XML_FILE])
+        delete_file_if_it_exists(input_arguments.get(common_constants.JSON_PATH, ''))
     return ""
 
 
-def save_issues_to_upload_folder(dictionary_filename, validation_issues, file_mode='w'):
-    """Saves the validation issues found to a other in the upload folder.
-
-    Parameters
-    ----------
-    dictionary_filename: str
-        The name to the dictionary.
-    validation_issues: dictionary
-        A string containing the validation issues.
-    file_mode: str
-        Either 'w' to create a new file and write or 'a' to create a new file if necessary and append to end.
-    Returns
-    -------
-    string
-        The name of the validation output other.
-
-    """
-    validation_issues_filename = generate_dictionary_validation_filename(dictionary_filename)
-    validation_issues_file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], validation_issues_filename)
-    with open(validation_issues_file_path, file_mode, encoding='utf-8') as validation_issues_file:
-        for val_issue in validation_issues.values():
-            for issue in val_issue:
-                validation_issues_file.write(issue['message'] + "\n")
-    validation_issues_file.close()
-    return validation_issues_file_path
-
-
-def validate_dictionary(validation_arguments):
-    """Validates the dictionary.
+def validate_dictionary(input_arguments, hed_schema=None, return_response=True):
+    """Validates the dictionary and returns a
 
     Parameters
     ----------
-    validation_arguments: dictionary
-        A dictionary containing the arguments for the validation function.
+    input_arguments: dict
+        Dictionary containing standard input form arguments
+    hed_schema: str or HedSchema
+        Version number or path or HedSchema object to be used
+    return_response: bool
+        If true, return a Response. If false return a printable issue string
 
     Returns
     -------
-    ColumnDefGroup
-        Contains a representation of the JSON dictionary and validation issues.
+    Response or str
+        Either a Response or a printable issue string
     """
 
-    json_dictionary = ColumnDefGroup(validation_arguments[common_constants.JSON_PATH])
-    hed_schema = HedSchema(validation_arguments[common_constants.HED_XML_FILE])
-    json_dictionary.validate_entries(hed_schema)
-    return json_dictionary
+    json_dictionary = ColumnDefGroup(input_arguments.get(common_constants.JSON_PATH, ''))
+    if not hed_schema:
+        hed_schema = HedSchema(input_arguments.get(common_constants.HED_XML_FILE, ''))
+    issues = json_dictionary.validate_entries(hed_schema)
+    if issues:
+        display_name = input_arguments.get(common_constants.JSON_FILE, '')
+        issue_str = get_printable_issue_string(issues, display_name, 'HED validation errors')
+        if not return_response:
+            return issue_str
+        file_name = generate_filename(display_name, suffix='validation_errors', extension='.txt')
+        issue_file = save_text_to_upload_folder(issue_str, file_name)
+        download_response = generate_download_file_response(issue_file, display_name=file_name, category='warning',
+                                                            msg='JSON dictionary had validation errors')
+        if isinstance(download_response, str):
+            return handle_http_error(error_constants.NOT_FOUND_ERROR, download_response)
+        return download_response
+    return ""
diff --git a/hedweb/hed/web/events.py b/hedweb/hed/web/events.py
new file mode 100644
index 000000000..a4c07d57f
--- /dev/null
+++ b/hedweb/hed/web/events.py
@@ -0,0 +1,93 @@
+
+from urllib.error import URLError, HTTPError
+from flask import current_app
+
+from hed.util.file_util import delete_file_if_it_exists
+from hed.validator.hed_validator import HedValidator
+from hed.util.hed_schema import HedSchema
+
+from hed.web.constants import common_constants, error_constants, file_constants
+from hed.web.dictionary import validate_dictionary
+from hed.web.spreadsheet import validate_spreadsheet
+from hed.web.web_utils import get_hed_path_from_pull_down, get_uploaded_file_path_from_form, get_optional_form_field
+
+app_config = current_app.config
+
+
+def generate_input_from_events_form(request):
+    """Gets the validation function input arguments from a request object associated with the validation form.
+
+    Parameters
+    ----------
+    request: Request object
+        A Request object containing user data from the validation form.
+
+    Returns
+    -------
+    dictionary
+        A dictionary containing input arguments for calling the underlying validation function.
+    """
+    hed_file_path, hed_display_name = get_hed_path_from_pull_down(request)
+    uploaded_events_name, original_events_name = \
+        get_uploaded_file_path_from_form(request, common_constants.SPREADSHEET_FILE,
+                                         file_constants.SPREADSHEET_FILE_EXTENSIONS)
+
+    uploaded_json_name, original_json_name = \
+        get_uploaded_file_path_from_form(request, common_constants.JSON_FILE,
+                                         file_constants.DICTIONARY_FILE_EXTENSIONS)
+
+    input_arguments = {
+        common_constants.HED_XML_FILE: hed_file_path,
+        common_constants.HED_DISPLAY_NAME: hed_display_name,
+        common_constants.SPREADSHEET_PATH: uploaded_events_name,
+        common_constants.SPREADSHEET_FILE: original_events_name,
+        common_constants.JSON_PATH: uploaded_json_name,
+        common_constants.JSON_FILE: original_json_name,
+        common_constants.WORKSHEET_NAME: get_optional_form_field(request, common_constants.WORKSHEET_NAME,
+                                                                 common_constants.STRING),
+        common_constants.HAS_COLUMN_NAMES: get_optional_form_field(request,
+                                                                   common_constants.HAS_COLUMN_NAMES,
+                                                                   common_constants.BOOLEAN),
+        common_constants.CHECK_FOR_WARNINGS: get_optional_form_field(request,
+                                                                     common_constants.CHECK_FOR_WARNINGS,
+                                                                     common_constants.BOOLEAN)
+    }
+    return input_arguments
+
+
+def report_events_validation_status(request):
+    """Reports the spreadsheet validation status.
+
+    Parameters
+    ----------
+    request: Request object
+        A Request object containing user data from the validation form.
+
+    Returns
+    -------
+    string
+        A serialized JSON string containing information related to the worksheet columns. If the validation fails then a
+        500 error message is returned.
+    """
+    input_arguments = []
+    try:
+        input_arguments = generate_input_from_events_form(request)
+        hed_schema = HedSchema(input_arguments.get(common_constants.HED_XML_FILE, ''))
+        download_response = validate_dictionary(input_arguments, hed_schema=hed_schema)
+        if download_response:
+            return download_response
+        hed_validator = HedValidator(hed_schema=hed_schema,
+                                     check_for_warnings=input_arguments.get(common_constants.CHECK_FOR_WARNINGS, False))
+        download_response = validate_spreadsheet(input_arguments, hed_validator)
+        if download_response:
+            return download_response
+    except HTTPError:
+        return error_constants.NO_URL_CONNECTION_ERROR
+    except URLError:
+        return error_constants.INVALID_URL_ERROR
+    except Exception as e:
+        return "Unexpected processing error: " + str(e)
+    finally:
+        delete_file_if_it_exists(input_arguments.get(common_constants.SPREADSHEET_PATH, ''))
+        delete_file_if_it_exists(input_arguments.get(common_constants.JSON_PATH, ''))
+    return ""
diff --git a/hedweb/hed/web/routes.py b/hedweb/hed/web/routes.py
index e490ea88d..9ca98cc6c 100644
--- a/hedweb/hed/web/routes.py
+++ b/hedweb/hed/web/routes.py
@@ -3,14 +3,13 @@
 import json
 import traceback
 
-import hed.util.file_util
 from hed.util import hed_cache
 
-from hed.web import dictionary, spreadsheet, schema, utils
+from hed.web import events, spreadsheet, schema, services, utils
 from hed.web.constants import blueprint_constants, common_constants, error_constants, page_constants, route_constants
-from hed.web.web_utils import delete_file_if_it_exist, find_hed_version_in_uploaded_file, save_file_to_upload_folder, \
+from hed.web.web_utils import delete_file_if_it_exists, find_hed_version_in_uploaded_file, save_file_to_upload_folder, \
     generate_download_file_response, handle_http_error
-
+from hed.web.dictionary import report_dictionary_validation_status
 
 app_config = current_app.config
 route_blueprint = Blueprint(blueprint_constants.ROUTE_BLUEPRINT, __name__)
@@ -29,7 +28,7 @@ def delete_file_in_upload_directory(filename):
     -------
 
     """
-    if hed.util.file_util.delete_file_if_it_exist(os.path.join(app_config['UPLOAD_FOLDER'], filename)):
+    if delete_file_if_it_exists(os.path.join(app_config['UPLOAD_FOLDER'], filename)):
         return Response(status=error_constants.NO_CONTENT_SUCCESS)
     else:
         return handle_http_error(error_constants.NOT_FOUND_ERROR, error_constants.FILE_DOES_NOT_EXIST)
@@ -71,21 +70,22 @@ def get_dictionary_validation_results():
         A serialized JSON string containing information related to the worksheet columns. If the validation fails then a
         500 error message is returned.
     """
-
-    validation_response = dictionary.report_dictionary_validation_status(request)
-    # Success
-    if isinstance(validation_response, Response):
-        return validation_response
-    if isinstance(validation_response, str):
-        if validation_response:
-            return handle_http_error(error_constants.INTERNAL_SERVER_ERROR, validation_response, as_text=True)
-        else:
-            return ""
-
-
-@route_blueprint.route(route_constants.SCHEMA_COMPLIANCE_CHECK_SUBMIT_ROUTE, strict_slashes=False, methods=['POST'])
-def get_schema_compliance_check_results():
-    """Check the HED specification in the form after submission and return an attachment other containing the output.
+    validation_response = report_dictionary_validation_status(request)
+    return validation_response
+    # validation_response = dictionary.report_dictionary_validation_status(request)
+    # # Success
+    # if isinstance(validation_response, Response):
+    #     return validation_response
+    # if isinstance(validation_response, str):
+    #     if validation_response:
+    #         return handle_http_error(error_constants.INTERNAL_SERVER_ERROR, validation_response, as_text=True)
+    #     else:
+    #         return ""
+
+
+@route_blueprint.route(route_constants.EVENTS_VALIDATION_SUBMIT_ROUTE, strict_slashes=False, methods=['POST'])
+def get_events_validation_results():
+    """Validate the spreadsheet in the form after submission and return an attachment other containing the output.
 
     Parameters
     ----------
@@ -93,25 +93,22 @@ def get_schema_compliance_check_results():
     Returns
     -------
         string
-        A serialized JSON string containing the hed specification to check. If the conversion fails then a
+        A serialized JSON string containing information related to the worksheet columns. If the validation fails then a
         500 error message is returned.
     """
-    comparison_response = schema.run_schema_compliance_check(request)
+    validation_response = events.report_events_validation_status(request)
     # Success
-    if isinstance(comparison_response, Response):
-        return comparison_response
-    if isinstance(comparison_response, str):
-        if comparison_response:
-            return handle_http_error(error_constants.INTERNAL_SERVER_ERROR, comparison_response, as_text=True)
+    if isinstance(validation_response, Response):
+        return validation_response
+    if isinstance(validation_response, str):
+        if validation_response:
+            return handle_http_error(error_constants.INTERNAL_SERVER_ERROR, validation_response, as_text=True)
         else:
             return ""
 
-    return handle_http_error(error_constants.INTERNAL_SERVER_ERROR,
-                             "Invalid duplicate tag check. This should not happen.", as_text=True)
 
-
-@route_blueprint.route(route_constants.EEG_VALIDATION_SUBMIT_ROUTE, strict_slashes=False, methods=['POST'])
-def get_eeg_events_validation_results():
+@route_blueprint.route(route_constants.HED_SERVICES_SUBMIT_ROUTE, strict_slashes=False, methods=['POST'])
+def get_hed_services_results():
     """Validate the hed strings associated with EEG events after submission from HEDTools EEGLAB plugin and
     return json string containing the output.
 
@@ -124,15 +121,15 @@ def get_eeg_events_validation_results():
         A serialized JSON string containing information related to the EEG events' hed-strings.
         If the validation fails then a 500 error message is returned.
     """
-    validation_status = spreadsheet.report_eeg_events_validation_status(request)
+    status = services.report_services_status(request)
 
-    if error_constants.ERROR_KEY in validation_status:
-        return handle_http_error(error_constants.INTERNAL_SERVER_ERROR, validation_status[error_constants.ERROR_KEY])
-    return json.dumps(validation_status)
+    if error_constants.ERROR_KEY in status:
+        return handle_http_error(error_constants.INTERNAL_SERVER_ERROR, status[error_constants.ERROR_KEY])
+    return json.dumps(status)
 
 
 @route_blueprint.route(route_constants.HED_VERSION_ROUTE, methods=['POST'])
-def get_hed_version_in_file():
+def get_hed_version():
     """Finds the information about the HED version of a file and returns as JSON.
 
     Parameters
@@ -174,6 +171,33 @@ def get_major_hed_versions():
     return json.dumps(hed_info)
 
 
+@route_blueprint.route(route_constants.SCHEMA_COMPLIANCE_CHECK_SUBMIT_ROUTE, strict_slashes=False, methods=['POST'])
+def get_schema_compliance_check_results():
+    """Check the HED specification in the form after submission and return an attachment other containing the output.
+
+    Parameters
+    ----------
+
+    Returns
+    -------
+        string
+        A serialized JSON string containing the hed specification to check. If the conversion fails then a
+        500 error message is returned.
+    """
+    comparison_response = schema.run_schema_compliance_check(request)
+    # Success
+    if isinstance(comparison_response, Response):
+        return comparison_response
+    if isinstance(comparison_response, str):
+        if comparison_response:
+            return handle_http_error(error_constants.INTERNAL_SERVER_ERROR, comparison_response, as_text=True)
+        else:
+            return ""
+
+    return handle_http_error(error_constants.INTERNAL_SERVER_ERROR,
+                             "Invalid duplicate tag check. This should not happen.", as_text=True)
+
+
 @route_blueprint.route(route_constants.SCHEMA_CONVERSION_SUBMIT_ROUTE, strict_slashes=False, methods=['POST'])
 def get_schema_conversion_results():
     """Convert the given HED specification and return an attachment with the result.
@@ -273,7 +297,7 @@ def get_worksheets_info():
     except:
         worksheets_info[error_constants.ERROR_KEY] = traceback.format_exc()
     finally:
-        delete_file_if_it_exist(workbook_file_path)
+        delete_file_if_it_exists(workbook_file_path)
     return json.dumps(worksheets_info)
 
 
@@ -309,8 +333,8 @@ def render_common_error_page():
     return render_template(page_constants.COMMON_ERRORS_PAGE)
 
 
-@route_blueprint.route(route_constants.DICTIONARY_VALIDATION_ROUTE, strict_slashes=False, methods=['GET'])
-def render_dictionary_validation_form():
+@route_blueprint.route(route_constants.DICTIONARY_ROUTE, strict_slashes=False, methods=['GET'])
+def render_dictionary_form():
     """Handles the site root and Validation tab functionality.
 
     Parameters
@@ -327,9 +351,9 @@ def render_dictionary_validation_form():
     # return render_template(page_constants.DICTIONARY_VALIDATION_PAGE)
 
 
-@route_blueprint.route(route_constants.EEG_VALIDATION_ROUTE, strict_slashes=False, methods=['GET'])
-def render_eeg_validation_form():
-    """Handles the site root and EEG Validation tab functionality.
+@route_blueprint.route(route_constants.EVENTS_ROUTE, strict_slashes=False, methods=['GET'])
+def render_events_form():
+    """Handles the site root and Validation tab functionality.
 
     Parameters
     ----------
@@ -341,12 +365,12 @@ def render_eeg_validation_form():
         displayed. If the HTTP method is a POST then the validation form is submitted.
 
     """
-    return render_template(page_constants.EEG_VALIDATION_PAGE)
+    return render_template(page_constants.EVENTS_PAGE)
 
 
-@route_blueprint.route(route_constants.HED_TOOLS_HOME_ROUTE, strict_slashes=False, methods=['GET'])
-def render_home_page():
-    """Handles the home page.
+@route_blueprint.route(route_constants.HED_SERVICES_ROUTE, strict_slashes=False, methods=['GET'])
+def render_hed_services_form():
+    """Handles the site root and EEG Validation tab functionality.
 
     Parameters
     ----------
@@ -354,10 +378,11 @@ def render_home_page():
     Returns
     -------
     Rendered template
-        A rendered template for the home page.
+        A rendered template for the validation form. If the HTTP method is a GET then the validation form will be
+        displayed. If the HTTP method is a POST then the validation form is submitted.
 
     """
-    return render_template(page_constants.HED_TOOLS_HOME_PAGE)
+    return render_template(page_constants.HED_SERVICES_PAGE)
 
 
 @route_blueprint.route(route_constants.HED_TOOLS_HELP_ROUTE, strict_slashes=False, methods=['GET'])
@@ -376,6 +401,22 @@ def render_help_page():
     return render_template(page_constants.HED_TOOLS_HELP_PAGE)
 
 
+@route_blueprint.route(route_constants.HED_TOOLS_HOME_ROUTE, strict_slashes=False, methods=['GET'])
+def render_home_page():
+    """Handles the home page.
+
+    Parameters
+    ----------
+
+    Returns
+    -------
+    Rendered template
+        A rendered template for the home page.
+
+    """
+    return render_template(page_constants.HED_TOOLS_HOME_PAGE)
+
+
 @route_blueprint.route(route_constants.SCHEMA_ROUTE, strict_slashes=False, methods=['GET'])
 def render_schema_form():
     """Handles the site root and conversion tab functionality.
@@ -393,8 +434,8 @@ def render_schema_form():
     return render_template(page_constants.SCHEMA_PAGE)
 
 
-@route_blueprint.route(route_constants.SPREADSHEET_VALIDATION_ROUTE, strict_slashes=False, methods=['GET'])
-def render_spreadsheet_validation_form():
+@route_blueprint.route(route_constants.SPREADSHEET_ROUTE, strict_slashes=False, methods=['GET'])
+def render_spreadsheet_form():
     """Handles the site root and Validation tab functionality.
 
     Parameters
@@ -407,4 +448,4 @@ def render_spreadsheet_validation_form():
         displayed. If the HTTP method is a POST then the validation form is submitted.
 
     """
-    return render_template(page_constants.SPREADSHEET_VALIDATION_PAGE)
+    return render_template(page_constants.SPREADSHEET_PAGE)
diff --git a/hedweb/hed/web/schema.py b/hedweb/hed/web/schema.py
index f11ed58ec..4930361e6 100644
--- a/hedweb/hed/web/schema.py
+++ b/hedweb/hed/web/schema.py
@@ -1,27 +1,28 @@
-from os.path import basename
+from os.path import basename, splitext
 import traceback
 from urllib.error import URLError, HTTPError
 from urllib.parse import urlparse
 from flask import current_app, Response
 
 from hed.schema import xml2wiki, wiki2xml, schema_validator
-from hed.util.file_util import delete_file_if_it_exist, url_to_file, get_file_extension, write_errors_to_file
-from hed.util.exceptions import SchemaFileError
+from hed.util.file_util import delete_file_if_it_exists, url_to_file, get_file_extension
+from hed.util.exceptions import HedFileError
 
 from hed.web.web_utils import file_extension_is_valid, form_has_file, form_has_option, form_has_url, \
-    handle_http_error, save_file_to_upload_folder, generate_download_file_response
+    generate_download_file_response, generate_filename, get_printable_issue_string, \
+    handle_http_error, save_file_to_upload_folder, save_text_to_upload_folder
 
 from hed.web.constants import common_constants, error_constants, file_constants
 
 app_config = current_app.config
 
 
-def generate_input_from_schema_form(form_request_object):
+def generate_input_from_schema_form(request):
     """Gets the conversion function input arguments from a request object associated with the conversion form.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from the conversion form.
 
     Returns
@@ -31,16 +32,16 @@ def generate_input_from_schema_form(form_request_object):
     """
     arguments = {}
    
-    if form_has_option(form_request_object, common_constants.SCHEMA_UPLOAD_OPTIONS,
+    if form_has_option(request, common_constants.SCHEMA_UPLOAD_OPTIONS,
                        common_constants.SCHEMA_FILE_OPTION) and \
-            form_has_file(form_request_object, common_constants.SCHEMA_FILE, file_constants.SCHEMA_EXTENSIONS):
-        schema_file = form_request_object.files[common_constants.SCHEMA_FILE]
+            form_has_file(request, common_constants.SCHEMA_FILE, file_constants.SCHEMA_EXTENSIONS):
+        schema_file = request.files[common_constants.SCHEMA_FILE]
         arguments[common_constants.SCHEMA_PATH] = save_file_to_upload_folder(schema_file)
         arguments[common_constants.SCHEMA_DISPLAY_NAME] = schema_file.filename
-    elif form_has_option(form_request_object, common_constants.SCHEMA_UPLOAD_OPTIONS,
+    elif form_has_option(request, common_constants.SCHEMA_UPLOAD_OPTIONS,
                          common_constants.SCHEMA_URL_OPTION) and \
-            form_has_url(form_request_object, common_constants.SCHEMA_URL, file_constants.SCHEMA_EXTENSIONS):
-        schema_url = form_request_object.values[common_constants.SCHEMA_URL]
+            form_has_url(request, common_constants.SCHEMA_URL, file_constants.SCHEMA_EXTENSIONS):
+        schema_url = request.values[common_constants.SCHEMA_URL]
         arguments[common_constants.SCHEMA_PATH] = url_to_file(schema_url)
         url_parsed = urlparse(schema_url)
         arguments[common_constants.SCHEMA_DISPLAY_NAME] = basename(url_parsed.path)
@@ -72,7 +73,7 @@ def get_schema_conversion(schema_local_path):
     return converted_schema_path, errors
 
 
-def run_schema_compliance_check(form_request_object):
+def run_schema_compliance_check(request):
     """Run tag comparison(map_schema from converter)
 
     returns: Response or string.
@@ -82,22 +83,25 @@ def run_schema_compliance_check(form_request_object):
     """
     hed_file_path = ''
     try:
-        conversion_input_arguments = generate_input_from_schema_form(form_request_object)
-        hed_file_path = conversion_input_arguments.get(common_constants.SCHEMA_PATH, '')
+        input_arguments = generate_input_from_schema_form(request)
+        hed_file_path = input_arguments.get(common_constants.SCHEMA_PATH, '')
         if hed_file_path and hed_file_path.endswith(".mediawiki"):
             new_file_path, errors = get_schema_conversion(hed_file_path)
             if new_file_path:
-                delete_file_if_it_exist(hed_file_path)
+                delete_file_if_it_exists(hed_file_path)
                 hed_file_path = new_file_path
 
         if not hed_file_path or not file_extension_is_valid(hed_file_path, [file_constants.SCHEMA_XML_EXTENSION]):
             return f"Invalid file name {hed_file_path}"
-        issues = schema_validator.validate_schema(hed_file_path)
 
+        issues = schema_validator.validate_schema(hed_file_path)
         if issues:
-            #issue_file = write_text_iter_to_file(issues)
-            issue_file = write_errors_to_file(issues, extension=".txt")
-            download_response = generate_download_file_response(issue_file, f"HED-3G format issues for: {hed_file_path}")
+            display_name = input_arguments.get(common_constants.SCHEMA_DISPLAY_NAME)
+            issue_str = get_printable_issue_string(issues, display_name, 'Schema HED 3G compliance errors for ')
+            file_name = generate_filename(display_name, suffix='schema_errors', extension='.txt')
+            issue_file = save_text_to_upload_folder(issue_str, file_name)
+            download_response = generate_download_file_response(issue_file, display_name=file_name, category='warning',
+                                                                msg='Schema is not HED 3G compliant')
             if isinstance(download_response, str):
                 return handle_http_error(error_constants.NOT_FOUND_ERROR, download_response)
             return download_response
@@ -105,16 +109,16 @@ def run_schema_compliance_check(form_request_object):
         return error_constants.NO_URL_CONNECTION_ERROR
     except URLError:
         return error_constants.INVALID_URL_ERROR
-    except SchemaFileError as e:
+    except HedFileError as e:
         return e.message
     except Exception as e:
         return "Unexpected processing error: " + str(e)
     finally:
-        delete_file_if_it_exist(hed_file_path)
+        delete_file_if_it_exists(hed_file_path)
     return ""
 
 
-def run_schema_conversion(form_request_object):
+def run_schema_conversion(request):
     """Run conversion(wiki2xml or xml2wiki from converter)
 
     returns: Response or string.
@@ -123,20 +127,29 @@ def run_schema_conversion(form_request_object):
     """
     hed_file_path = ''
     try:
-        arguments = generate_input_from_schema_form(form_request_object)
-        hed_file_path = arguments.get(common_constants.SCHEMA_PATH)
-        schema_file, errors = get_schema_conversion(hed_file_path)
-        if errors:
-            download_response = errors
+        input_arguments = generate_input_from_schema_form(request)
+        hed_file_path = input_arguments.get(common_constants.SCHEMA_PATH)
+        display_name = input_arguments.get(common_constants.SCHEMA_DISPLAY_NAME)
+        schema_file, issues = get_schema_conversion(hed_file_path)
+        if issues:
+            issue_str = get_printable_issue_string(issues, display_name, 'Schema conversion errors for ')
+            file_name = generate_filename(display_name, suffix='conversion_errors', extension='.txt')
+            issue_file = save_text_to_upload_folder(issue_str, file_name)
+            download_response = \
+                generate_download_file_response(issue_file, display_name=file_name, category='warning',
+                                                msg='Schema had validation errors and could not be converted')
         else:
-            download_response = generate_download_file_response(schema_file)
+            schema_name, schema_ext = splitext(schema_file)
+            file_name = generate_filename(display_name,  extension=schema_ext)
+            download_response = generate_download_file_response(schema_file, display_name=file_name, category='success',
+                                                                msg='Schema was successfully converted')
         if isinstance(download_response, Response):
             return download_response
         elif isinstance(download_response, str):
             return handle_http_error(error_constants.NOT_FOUND_ERROR, download_response)
         else:
             raise Exception("Unable to download schema file for unknown reasons")
-    except SchemaFileError as e:
+    except HedFileError as e:
         return e.message
     except HTTPError:
         return error_constants.NO_URL_CONNECTION_ERROR
@@ -145,4 +158,4 @@ def run_schema_conversion(form_request_object):
     except:
         return traceback.format_exc()
     finally:
-        delete_file_if_it_exist(hed_file_path)
+        delete_file_if_it_exists(hed_file_path)
diff --git a/hedweb/hed/web/services.py b/hedweb/hed/web/services.py
new file mode 100644
index 000000000..4641918bf
--- /dev/null
+++ b/hedweb/hed/web/services.py
@@ -0,0 +1,92 @@
+import json
+import traceback
+from urllib.error import URLError, HTTPError
+from flask import current_app
+
+from hed.util.file_util import get_file_extension, delete_file_if_it_exists
+from hed.validator.hed_validator import HedValidator
+from hed.util.hed_file_input import HedFileInput
+
+from hed.web.constants import common_constants, error_constants, file_constants
+from hed.web.web_utils import convert_number_str_to_list, generate_filename, get_printable_issue_string, \
+    generate_download_file_response, get_hed_path_from_pull_down, get_uploaded_file_path_from_form, \
+    handle_http_error, save_file_to_upload_folder, save_text_to_upload_folder
+from hed.web import utils
+
+app_config = current_app.config
+
+
+def generate_arguments_from_form(request):
+    """Gets the input arguments from a request object associated with a service.
+
+    Parameters
+    ----------
+    request: Request object
+        A Request object containing user data from the validation form.
+
+    Returns
+    -------
+    dictionary
+        A dictionary containing input arguments for calling the underlying validation function.
+    """
+    hed_file_path, hed_display_name = get_hed_path_from_pull_down(request)
+    uploaded_file_name, original_file_name = \
+        get_uploaded_file_path_from_form(request, common_constants.SPREADSHEET_FILE,
+                                         file_constants.SPREADSHEET_FILE_EXTENSIONS)
+
+    input_arguments = { }
+    #     common_constants.HED_VERSION:  form_data = request.form,
+    #     common_constants.HED_DISPLAY_NAME: hed_display_name,
+    #
+    #
+    #     common_constants.CHECK_FOR_WARNINGS:
+    #         utils.get_optional_form_field(request, common_constants.CHECK_FOR_WARNINGS,
+    #                                       common_constants.BOOLEAN)
+    # }
+    return input_arguments
+
+
+def report_services_status(request):
+    """
+    Reports validation status of hed strings associated with EEG events received from EEGLAB plugin HEDTools
+
+    Parameters
+    ----------
+    request: Request object
+        A Request object containing user data submitted by HEDTools.
+        Keys include "hed_strings", "check_for_warnings", and "hed_xml_file"
+
+    Returns
+    -------
+    string
+        A serialized JSON string containing information related to the hed strings validation result.
+        If the validation fails then a 500 error message is returned.
+    """
+    validation_status = {}
+
+    # Parse uploaded data
+    form_data = request.form
+    check_for_warnings = form_data["check_for_warnings"] == '1' if "check_for_warnings" in form_data else False
+    # if hed_xml_file was submitted, it's accessed by request.files, otherwise empty
+    if "hed-xml-file" in request.files and get_file_extension(request.files["hed-xml-file"].filename) == "xml":
+        hed_xml_file = save_file_to_upload_folder(request.files["hed-xml-file"])
+    else:
+        hed_xml_file = ''
+
+    try:
+        # parse hed_strings from json
+        hed_strings = json.loads(form_data["hed-strings"])
+        # hed_strings is a list of HED strings associated with events in EEG.event (order preserved)
+        hed_input_reader = HedValidator(hed_strings, check_for_warnings=check_for_warnings, hed_xml_file=hed_xml_file)
+        # issues is a list of lists. Element list is empty if no error,
+        # else is a list of dictionaries, each dictionary contains an error-message key-value pair
+        issues = hed_input_reader.get_validation_issues()
+
+        # Prepare response
+        validation_status["issues"] = issues
+    except:
+        validation_status[error_constants.ERROR_KEY] = traceback.format_exc()
+    finally:
+        delete_file_if_it_exists(hed_xml_file)
+
+    return validation_status
diff --git a/hedweb/hed/web/spreadsheet.py b/hedweb/hed/web/spreadsheet.py
index ca1e604d5..2f50f7173 100644
--- a/hedweb/hed/web/spreadsheet.py
+++ b/hedweb/hed/web/spreadsheet.py
@@ -1,27 +1,26 @@
-import json
-import os
-import traceback
 from urllib.error import URLError, HTTPError
 from flask import current_app
-from werkzeug.utils import secure_filename
 
-from hed.util.file_util import get_file_extension, delete_file_if_it_exist
+from hed.util.file_util import delete_file_if_it_exists
 from hed.validator.hed_validator import HedValidator
 from hed.util.hed_file_input import HedFileInput
 
+import hed.web.web_utils
 from hed.web.constants import common_constants, error_constants, file_constants
-from hed.web import web_utils
+from hed.web.web_utils import convert_number_str_to_list, generate_filename, get_printable_issue_string, \
+    generate_download_file_response, get_hed_path_from_pull_down, get_uploaded_file_path_from_form, \
+    handle_http_error, save_text_to_upload_folder
 from hed.web import utils
 
 app_config = current_app.config
 
 
-def generate_arguments_from_validation_form(form_request_object):
+def generate_input_from_validation_form(request):
     """Gets the validation function input arguments from a request object associated with the validation form.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from the validation form.
 
     Returns
@@ -29,103 +28,34 @@ def generate_arguments_from_validation_form(form_request_object):
     dictionary
         A dictionary containing input arguments for calling the underlying validation function.
     """
-    hed_file_path, hed_display_name = web_utils.get_hed_path_from_pull_down(form_request_object)
+    hed_file_path, hed_display_name = get_hed_path_from_pull_down(request)
     uploaded_file_name, original_file_name = \
-        web_utils.get_uploaded_file_path_from_form(form_request_object, common_constants.SPREADSHEET_FILE,
-                                                   file_constants.SPREADSHEET_FILE_EXTENSIONS)
-
-    validation_input_arguments = {common_constants.HED_XML_FILE: hed_file_path,
-                                  common_constants.HED_DISPLAY_NAME: hed_display_name,
-                                  common_constants.SPREADSHEET_PATH: uploaded_file_name,
-                                  common_constants.SPREADSHEET_FILE: original_file_name}
-    validation_input_arguments[common_constants.TAG_COLUMNS] = \
-        web_utils.convert_number_str_to_list(form_request_object.form[common_constants.TAG_COLUMNS])
-    validation_input_arguments[common_constants.COLUMN_PREFIX_DICTIONARY] = \
-        utils.get_specific_tag_columns_from_form(form_request_object)
-    validation_input_arguments[common_constants.WORKSHEET_NAME] = \
-        utils.get_optional_form_field(form_request_object, common_constants.WORKSHEET_NAME,
-                                      common_constants.STRING)
-    validation_input_arguments[common_constants.HAS_COLUMN_NAMES] = utils.get_optional_form_field(
-        form_request_object, common_constants.HAS_COLUMN_NAMES, common_constants.BOOLEAN)
-    validation_input_arguments[common_constants.CHECK_FOR_WARNINGS] = utils.get_optional_form_field(
-        form_request_object, common_constants.CHECK_FOR_WARNINGS, common_constants.BOOLEAN)
+        get_uploaded_file_path_from_form(request, common_constants.SPREADSHEET_FILE,
+                                         file_constants.SPREADSHEET_FILE_EXTENSIONS)
+
+    validation_input_arguments = {
+        common_constants.HED_XML_FILE: hed_file_path,
+        common_constants.HED_DISPLAY_NAME: hed_display_name,
+        common_constants.SPREADSHEET_PATH: uploaded_file_name,
+        common_constants.SPREADSHEET_FILE: original_file_name,
+        common_constants.TAG_COLUMNS: convert_number_str_to_list(request.form[common_constants.TAG_COLUMNS]),
+        common_constants.COLUMN_PREFIX_DICTIONARY: utils.get_specific_tag_columns_from_form(request),
+        common_constants.WORKSHEET_NAME: hed.web.web_utils.get_optional_form_field(request, common_constants.WORKSHEET_NAME,
+                                                                                   common_constants.STRING),
+        common_constants.HAS_COLUMN_NAMES:
+            hed.web.web_utils.get_optional_form_field(request, common_constants.HAS_COLUMN_NAMES, common_constants.BOOLEAN),
+        common_constants.CHECK_FOR_WARNINGS:
+            hed.web.web_utils.get_optional_form_field(request, common_constants.CHECK_FOR_WARNINGS, common_constants.BOOLEAN)
+    }
     return validation_input_arguments
 
 
-def generate_spreadsheet_validation_filename(spreadsheet_filename, worksheet_name=''):
-    """Generates a filename for the attachment that will contain the spreadsheet validation issues.
-
-    Parameters
-    ----------
-    spreadsheet_filename: string
-        The name of the spreadsheet other.
-    worksheet_name: string
-        The name of the spreadsheet worksheet.
-    Returns
-    -------
-    string
-        The name of the attachment other containing the validation issues.
-    """
-    if worksheet_name:
-        return common_constants.VALIDATION_OUTPUT_FILE_PREFIX + \
-               secure_filename(spreadsheet_filename).rsplit('.')[0] + '_' + \
-               secure_filename(worksheet_name) + file_constants.TEXT_EXTENSION
-    return common_constants.VALIDATION_OUTPUT_FILE_PREFIX + secure_filename(spreadsheet_filename).rsplit('.')[
-        0] + file_constants.TEXT_EXTENSION
-
-
-def report_eeg_events_validation_status(request):
-    """Reports validation status of hed strings associated with EEG events
-       received from EEGLAB plugin HEDTools
-
-    Parameters
-    ----------
-    request: Request object
-        A Request object containing user data submitted by HEDTools.
-        Keys include "hed_strings", "check_for_warnings", and "hed_xml_file"
-
-    Returns
-    -------
-    string
-        A serialized JSON string containing information related to the hed strings validation result.
-        If the validation fails then a 500 error message is returned.
-    """
-    validation_status = {}
-
-    # Parse uploaded data
-    form_data = request.form
-    check_for_warnings = form_data["check_for_warnings"] == '1' if "check_for_warnings" in form_data else False
-    # if hed_xml_file was submitted, it's accessed by request.files, otherwise empty
-    if "hed-xml-file" in request.files and get_file_extension(request.files["hed-xml-file"].filename) == "xml":
-        hed_xml_file = web_utils.save_file_to_upload_folder(request.files["hed-xml-file"])
-    else:
-        hed_xml_file = ''
-
-    try:
-        # parse hed_strings from json
-        hed_strings = json.loads(form_data["hed-strings"])
-        # hed_strings is a list of HED strings associated with events in EEG.event (order preserved)
-        hed_input_reader = HedValidator(hed_strings, check_for_warnings=check_for_warnings, hed_xml_file=hed_xml_file)
-        # issues is a list of lists. Element list is empty if no error,
-        # else is a list of dictionaries, each dictionary contains an error-message key-value pair
-        issues = hed_input_reader.get_validation_issues()
-
-        # Prepare response
-        validation_status["issues"] = issues
-    except:
-        validation_status[error_constants.ERROR_KEY] = traceback.format_exc()
-    finally:
-        delete_file_if_it_exist(hed_xml_file)
-
-    return validation_status
-
-
-def report_spreadsheet_validation_status(form_request_object):
+def report_spreadsheet_validation_status(request):
     """Reports the spreadsheet validation status.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from the validation form.
 
     Returns
@@ -136,17 +66,8 @@ def report_spreadsheet_validation_status(form_request_object):
     """
     input_arguments = []
     try:
-        input_arguments = generate_arguments_from_validation_form(form_request_object)
-        hed_validator = validate_spreadsheet(input_arguments)
-        validation_issues = hed_validator.get_validation_issues()
-        if validation_issues:
-            issue_file = save_issues_to_upload_folder(input_arguments[common_constants.SPREADSHEET_FILE],
-                                                      validation_issues,
-                                                      input_arguments[common_constants.WORKSHEET_NAME])
-            download_response = web_utils.generate_download_file_response(issue_file)
-            if isinstance(download_response, str):
-                return web_utils.handle_http_error(error_constants.NOT_FOUND_ERROR, download_response)
-            return download_response
+        input_arguments = generate_input_from_validation_form(request)
+        return validate_spreadsheet(input_arguments)
     except HTTPError:
         return error_constants.NO_URL_CONNECTION_ERROR
     except URLError:
@@ -154,59 +75,54 @@ def report_spreadsheet_validation_status(form_request_object):
     except Exception as e:
         return "Unexpected processing error: " + str(e)
     finally:
-        delete_file_if_it_exist(input_arguments[common_constants.SPREADSHEET_PATH])
-        # delete_file_if_it_exist(input_arguments[common_constants.HED_XML_FILE])
-    return ""
-
-
-def save_issues_to_upload_folder(spreadsheet_filename, validation_issues, worksheet_name=''):
-    """Saves the validation issues found to a other in the upload folder.
+        delete_file_if_it_exists(input_arguments.get(common_constants.SPREADSHEET_PATH, ''))
 
-    Parameters
-    ----------
-    spreadsheet_filename: string
-        The name to the spreadsheet.
-    worksheet_name: string
-        The name of the spreadsheet worksheet.
-    validation_issues: string
-        A string containing the validation issues.
 
-    Returns
-    -------
-    string
-        The name of the validation output other.
-
-    """
-    validation_issues_filename = generate_spreadsheet_validation_filename(spreadsheet_filename, worksheet_name)
-    validation_issues_file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], validation_issues_filename)
-    with open(validation_issues_file_path, 'w', encoding='utf-8') as validation_issues_file:
-        for val_issue in validation_issues:
-            validation_issues_file.write(val_issue['message'] + "\n")
-    validation_issues_file.close()
-    return validation_issues_file_path
-
-
-def validate_spreadsheet(validation_arguments):
+def validate_spreadsheet(input_arguments, hed_validator=None, return_response=True):
     """Validates the spreadsheet.
 
     Parameters
     ----------
-    validation_arguments: dictionary
+    input_arguments: dictionary
         A dictionary containing the arguments for the validation function.
-
+    hed_validator: HedValidator
+        Validator passed if previously created in another phase
+    return_response: bool
+        If true returns a response object, otherwise directly returns a printable issue string
     Returns
     -------
     HedValidator object
         A HedValidator object containing the validation results.
     """
 
-    file_input_object = HedFileInput(validation_arguments[common_constants.SPREADSHEET_PATH],
-                                     worksheet_name=validation_arguments[common_constants.WORKSHEET_NAME],
-                                     tag_columns=validation_arguments[common_constants.TAG_COLUMNS],
-                                     has_column_names=validation_arguments[common_constants.HAS_COLUMN_NAMES],
-                                     column_prefix_dictionary=validation_arguments[
-                                         common_constants.COLUMN_PREFIX_DICTIONARY])
-
-    return HedValidator(file_input_object,
-                        check_for_warnings=validation_arguments[common_constants.CHECK_FOR_WARNINGS],
-                        hed_xml_file=validation_arguments[common_constants.HED_XML_FILE])
+    file_input = HedFileInput(input_arguments.get(common_constants.SPREADSHEET_PATH, None),
+                              worksheet_name=input_arguments.get(common_constants.WORKSHEET_NAME, None),
+                              tag_columns=input_arguments.get(common_constants.TAG_COLUMNS, None),
+                              has_column_names=input_arguments.get(common_constants.HAS_COLUMN_NAMES, None),
+                              column_prefix_dictionary=input_arguments.get(common_constants.COLUMN_PREFIX_DICTIONARY,
+                                                                           None))
+    if not hed_validator:
+        hed_validator = HedValidator(hed_xml_file=input_arguments.get(common_constants.HED_XML_FILE, ''),
+                                     check_for_warnings=input_arguments.get(common_constants.CHECK_FOR_WARNINGS, False))
+
+    issues = hed_validator.validate_input(file_input)
+
+    if issues:
+        display_name = input_arguments.get(common_constants.SPREADSHEET_FILE, None)
+        worksheet_name = input_arguments.get(common_constants.WORKSHEET_NAME, None)
+        title_string = display_name
+        suffix = 'validation_errors'
+        if worksheet_name:
+            title_string = display_name + ' [worksheet ' + worksheet_name + ']'
+            suffix = '_worksheet_' + worksheet_name + '_' + suffix
+        issue_str = get_printable_issue_string(issues, title_string, 'HED validation errors')
+        if not return_response:
+            return issue_str
+        file_name = generate_filename(display_name, suffix=suffix, extension='.txt')
+        issue_file = save_text_to_upload_folder(issue_str, file_name)
+        download_response = generate_download_file_response(issue_file, display_name=file_name, category='warning',
+                                                            msg='Spreadsheet had validation errors')
+        if isinstance(download_response, str):
+            return handle_http_error(error_constants.NOT_FOUND_ERROR, download_response)
+        return download_response
+    return ""
diff --git a/hedweb/hed/web/static/css/spreadsheet-form.css b/hedweb/hed/web/static/css/forms.css
similarity index 100%
rename from hedweb/hed/web/static/css/spreadsheet-form.css
rename to hedweb/hed/web/static/css/forms.css
diff --git a/hedweb/hed/web/templates/dictionary.html b/hedweb/hed/web/templates/dictionary.html
index 67ba295e5..8fa6b03b5 100644
--- a/hedweb/hed/web/templates/dictionary.html
+++ b/hedweb/hed/web/templates/dictionary.html
@@ -4,10 +4,12 @@
 
 {% block content %}
     <h2>Validate JSON dictionary containing HED tags</h2>
-        <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/dictionary-form.css') }}">
+        <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/forms.css') }}">
     <form id="dictionary-form" method="POST" enctype="multipart/form-data">
         <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
 
+         <div class="form-group">
+
         {{ create_json_input() }}
         {{ create_hed_pulldown() }}
 
@@ -28,8 +30,8 @@ <h2>Validate JSON dictionary containing HED tags</h2>
     </form>
     <script type="module">
         {%  include 'js/form-helpers.js' %}
-        {%  include 'js/hed-pulldown.js' %}
         {%  include 'js/json-input.js' %}
+        {%  include 'js/hed-pulldown.js' %}
         {%  include 'js/dictionary-form.js' %}
     </script>
 {% endblock %}
\ No newline at end of file
diff --git a/hedweb/hed/web/templates/events.html b/hedweb/hed/web/templates/events.html
index ef2311cf7..86a3af406 100644
--- a/hedweb/hed/web/templates/events.html
+++ b/hedweb/hed/web/templates/events.html
@@ -1,50 +1,15 @@
 {% extends "layout.html" %}
 {% from "hed-pulldown.html" import create_hed_pulldown %}
+{% from "json-input.html" import create_json_input %}
+{% from "spreadsheet-input.html" import spreadsheet_input %}
 {% block content %}
     <h2>Validate spreadsheet containing HED tags</h2>
-    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/spreadsheet-form.css') }}">
-    <form id="spreadsheet-form" method="POST" enctype="multipart/form-data">
+    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/forms.css') }}">
+    <form id="events-form" method="POST" enctype="multipart/form-data">
         <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
 
-        <div class="form-group">
-            <label for="spreadsheet-file">Select spreadsheet (Excel or tab-separated):</label><br>
-            <label class="btn-group file-browse-group spreadsheet-btn-group">
-                <button for="spreadsheet-file">Browse ...</button>
-                <label id="spreadsheet-display-name"></label>
-                <input type="file" name="spreadsheet-file" id="spreadsheet-file"/>
-            </label>
-            <p class="flash" id="spreadsheet-flash"></p>
-        </div>
-
-        <div class="form-group">
-            <label for="worksheet-name">Choose a worksheet (for workbooks):</label><br>
-            <select name="worksheet-name" id="worksheet-name"></select>
-            <p class="flash" id="worksheet-flash"></p>
-        </div>
-
-        <div class="form-group" id="column-names">
-            <label class="secondary-label" for="columns-names-table">Worksheet columns</label>
-            <table id="columns-names-table"></table>
-        </div>
-
-        <div class="form-group">
-            <label>Special non-prefixed tag columns (if available):</label><br>
-            <div class="textbox-group">
-                <label for="category-column">Category</label>
-                <input type="text" name="category-column" id="category-column">
-                <label for="description-column">Description</label>
-                <input type="text" name="description-column" id="description-column">
-                <label for="label-column">Label</label>
-                <input type="text" name="label-column" id="label-column">
-                <label for="long-column">Long name</label>
-                <input type="text" name="long-column" id="long-column">
-            </div>
-            <label class="secondary-label" for="tag-columns">Columns containing HED strings
-                (comma-separated):</label><br>
-            <input class="textbox-group" type="text" name="tag-columns" id="tag-columns">
-            <p class="flash" id="tag-columns-flash"></p>
-        </div>
-
+        {{ spreadsheet_input() }}
+        {{ create_json_input() }}
         {{ create_hed_pulldown() }}
 
         <div class="form-group">
@@ -62,14 +27,18 @@ <h2>Validate spreadsheet containing HED tags</h2>
         </div>
 
         <div class="form-group">
-            <button id="spreadsheet-validation-submit" type="button">Validate</button>
-            <p class="flash" id="spreadsheet-validation-submit-flash"></p>
+            <button id="events-validation-submit" type="button">Validate</button>
+            <p class="flash" id="events-validation-submit-flash"></p>
         </div>
     </form>
     <script type="module">
+
         {%  include 'js/form-helpers.js' %}
         {%  include 'js/hed-pulldown.js' %}
-        {%  include 'js/spreadsheet-form.js' %}
+        {%  include 'js/json-input.js' %}
+        {%  include 'js/spreadsheet-input.js' %}
+        {%  include 'js/events-form.js' %}
+
     </script>
 
 {% endblock %}
\ No newline at end of file
diff --git a/hedweb/hed/web/templates/eeg-validation.html b/hedweb/hed/web/templates/hed-services.html
similarity index 56%
rename from hedweb/hed/web/templates/eeg-validation.html
rename to hedweb/hed/web/templates/hed-services.html
index b579c4f8b..f85acafb0 100644
--- a/hedweb/hed/web/templates/eeg-validation.html
+++ b/hedweb/hed/web/templates/hed-services.html
@@ -1,11 +1,11 @@
 <!DOCTYPE html>
-<html>
+<html lang="en">
 <head>
     <meta charset="UTF-8">
-    <title>EEG Validation</title>
+    <title>HED services</title>
 </head>
 <body>
-<form id="eegvalidation-form" method="POST" enctype="multipart/form-data">
+<form id="hed-services-form" method="POST" enctype="multipart/form-data">
     <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
 </form>
 </body>
diff --git a/hedweb/hed/web/templates/hed-tools-home.html b/hedweb/hed/web/templates/hed-tools-home.html
index 6fbc52217..1d5a26cdc 100644
--- a/hedweb/hed/web/templates/hed-tools-home.html
+++ b/hedweb/hed/web/templates/hed-tools-home.html
@@ -28,9 +28,9 @@ <h2>HED online tools</h2>
     <p>
         Several online HED tools are currently available:
         <ul>
-            <li><a href="{{ url_for('route_blueprint.render_spreadsheet_validation_form') }}"><b>Validate spreadsheet</b></a>
+            <li><a href="{{ url_for('route_blueprint.render_spreadsheet_form') }}"><b>Validate spreadsheet</b></a>
             validates HED tags in a tab-separated value or Excel spreadsheet and produces a download file with any errors.</li>
-        <li><a href="{{ url_for('route_blueprint.render_dictionary_validation_form') }}"><b>Validate dictionary</b></a>
+        <li><a href="{{ url_for('route_blueprint.render_dictionary_form') }}"><b>Validate dictionary</b></a>
             validates a JSON dictionary containing HED tags.</li>
         <li><a href="{{ url_for('route_blueprint.render_schema_form') }}"><b>Process schema</b></a> converts HED vocabulary
             schema between .mediawiki and .xml format. This tool also has options for checking that individual HED
diff --git a/hedweb/hed/web/templates/js/dictionary-form.js b/hedweb/hed/web/templates/js/dictionary-form.js
index a8547bc58..98b006943 100644
--- a/hedweb/hed/web/templates/js/dictionary-form.js
+++ b/hedweb/hed/web/templates/js/dictionary-form.js
@@ -1,10 +1,8 @@
-const DICTIONARY_FILE_EXTENSIONS = ['json'];
 
 $(function () {
-    prepareDictionaryValidationForm();
+    prepareForm();
 });
 
-
 /**
  * Submits the form if the tag columns textbox is valid.
  */
@@ -14,13 +12,12 @@ $('#dictionary-validation-submit').on('click', function () {
     }
 });
 
-
 /**
  * Prepare the validation form after the page is ready. The form will be reset to handle page refresh and
  * components will be hidden and populated.
  */
-function prepareDictionaryValidationForm() {
-    resetDictionaryForm();
+function prepareForm() {
+    resetForm();
     getHEDVersions()
     hideOtherHEDVersionFileUpload();
 }
@@ -28,7 +25,7 @@ function prepareDictionaryValidationForm() {
 /**
  * Resets the flash messages that aren't related to the form submission.
  */
-function resetFlashMessages() {
+function resetFormFlashMessages() {
     clearJsonFlashMessage();
     clearHEDFlashMessage();
     flashMessageOnScreen('', 'success', 'dictionary-validation-submit-flash');
@@ -37,7 +34,7 @@ function resetFlashMessages() {
 /**
  * Resets the fields in the form.
  */
-function resetDictionaryForm() {
+function resetForm() {
     $('#dictionary-form')[0].reset();
     clearJsonFileLabel();
     hideOtherHEDVersionFileUpload()
@@ -52,10 +49,9 @@ function submitForm() {
     let formData = new FormData(dictionaryForm);
 
     let dictionaryFile = getJsonFileLabel();
-    let display_name = convertToResultsName(dictionaryFile, 'validation_issues')
-    resetFlashMessages();
-    flashMessageOnScreen('Dictionary is being validated ...', 'success',
-        'dictionary-validation-submit-flash')
+    let display_name = convertToResultsName(dictionaryFile, 'issues')
+    resetFormFlashMessages();
+    flashMessageOnScreen('Dictionary is being validated ...', 'success', 'dictionary-validation-submit-flash')
     $.ajax({
             type: 'POST',
             url: "{{url_for('route_blueprint.get_dictionary_validation_results')}}",
@@ -63,36 +59,12 @@ function submitForm() {
             contentType: false,
             processData: false,
             dataType: 'text',
-            success: function (downloaded_file) {
-                  if (downloaded_file) {
-                      flashMessageOnScreen('', 'success',
-                          'dictionary-validation-submit-flash');
-                      triggerDownloadBlob(downloaded_file, display_name);
-                  } else {
-                      flashMessageOnScreen('No validation errors found.', 'success',
-                          'dictionary-validation-submit-flash');
-                  }
+            success: function (download, status, xhr) {
+                getResponseSuccess(download, xhr, display_name, 'dictionary-validation-submit-flash')
             },
-            error: function (download_response) {
-                console.log(download_response.responseText);
-                if (download_response.responseText.length < 100) {
-                    flashMessageOnScreen(download_response.responseText, 'error',
-                        'dictionary-validation-submit-flash');
-                } else {
-                    flashMessageOnScreen('Dictionary could not be processed',
-                        'error','dictionary-validation-submit-flash');
-                }
+            error: function (download, status, xhr) {
+                getResponseFailure(download, xhr, display_name, 'dictionary-validation-submit-flash')
             }
         }
     )
 }
-
-
-/**
- * Updates the dictionary file label.
- * @param {String} dictionaryPath - The path to the dictionary.
- */
-function updateDictionaryFileLabel(dictionaryPath) {
-    let dictionaryFilename = dictionaryPath.split('\\').pop();
-    $('#dictionary-display-name').text(dictionaryFilename);
-}
diff --git a/hedweb/hed/web/templates/js/events-form.js b/hedweb/hed/web/templates/js/events-form.js
new file mode 100644
index 000000000..d48415e93
--- /dev/null
+++ b/hedweb/hed/web/templates/js/events-form.js
@@ -0,0 +1,179 @@
+
+$(function () {
+    prepareForm();
+});
+
+/**
+ * Submits the form if there is a spreadsheet file and an available hed schema
+ */
+$('#events-validation-submit').on('click', function () {
+    if (spreadsheetIsSpecified()  && hedSpecifiedWhenOtherIsSelected()) {
+        submitForm();
+    }
+});
+
+/**
+ * Gets the spreadsheet columns.
+ * @param {Object} spreadsheetFile - A spreadsheet file.
+ * @param {String} worksheetName - An Excel worksheet name.
+ */
+function getColumnsInfo(spreadsheetFile, worksheetName) {
+    let formData = new FormData();
+    formData.append('spreadsheet-file', spreadsheetFile);
+    if (typeof worksheetName !== 'undefined') {
+        formData.append('worksheet-name', worksheetName);
+    }
+    $.ajax({
+        type: 'POST',
+        url: "{{url_for('route_blueprint.get_spreadsheet_columns_info')}}",
+        data: formData,
+        contentType: false,
+        processData: false,
+        dataType: 'json',
+        success: function (columnsInfo) {
+            setComponentsRelatedToColumns(columnsInfo);
+        },
+        error: function (jqXHR) {
+            console.log(jqXHR.responseJSON.message);
+            flashMessageOnScreen('Event annotations could not be processed.', 'error',
+                'spreadsheet-flash');
+        }
+    });
+}
+
+/**
+ * Gets information associated with the Excel workbook worksheets. This information contains the names of the
+ * worksheets, the names of the columns in the first worksheet, and column indices that contain HED tags in the
+ * first worksheet.
+ * @param {Object} workbookFile - An Excel workbook file.
+ */
+function getWorksheetsInfo(workbookFile) {
+    let formData = new FormData();
+    formData.append('spreadsheet-file', workbookFile);
+    $.ajax({
+        type: 'POST',
+        url: "{{url_for('route_blueprint.get_worksheets_info')}}",
+        data: formData,
+        contentType: false,
+        processData: false,
+        dataType: 'json',
+        success: function (worksheetsInfo) {
+            populateWorksheetSelectbox(worksheetsInfo['worksheet-names']);
+            setComponentsRelatedToColumns(worksheetsInfo);
+            flashWorksheetNumberMessage(worksheetsInfo['worksheet-names']);
+        },
+        error: function (jqXHR) {
+            console.log(jqXHR);
+            // console.log(jqXHR.responseJSON.message);
+            flashMessageOnScreen('Event annotations could not be processed.', 'error',
+                'events-validation-submit-flash');
+        }
+    });
+}
+
+
+/**
+ * Prepare the validation form after the page is ready. The form will be reset to handle page refresh and
+ * components will be hidden and populated.
+ */
+function prepareForm() {
+    resetForm();
+    getHEDVersions()
+    hideColumnNames();
+    hideOtherHEDVersionFileUpload();
+}
+
+/**
+ * Resets the flash messages that aren't related to the form submission.
+ */
+function resetFormFlashMessages() {
+    flashMessageOnScreen('', 'success', 'spreadsheet-flash');
+    flashMessageOnScreen('', 'success', 'worksheet-flash');
+    flashMessageOnScreen('', 'success', 'hed-flash');
+    flashMessageOnScreen('', 'success', 'events-validation-submit-flash');
+}
+
+/**
+ * Resets the fields in the form.
+ */
+function resetForm() {
+    $('#events-form')[0].reset();
+    clearSpreadsheetFileLabel();
+    clearWorksheetSelectbox();
+    hideColumnNames();
+    hideOtherHEDVersionFileUpload()
+}
+
+/**
+ * Sets the components related to Excel worksheet columns when they are all empty.
+ */
+function setComponentsRelatedToEmptyColumnNames() {
+    setHasColumnNamesCheckbox(false);
+    hideColumnNames();
+}
+
+/**
+ * Sets the components related to the spreadsheet columns when they are not empty.
+ * @param {Array} columnNames - An array containing the spreadsheet column names.
+ */
+function setComponentsRelatedToNonEmptyColumnNames(columnNames) {
+    showColumnNames(columnNames)
+    setHasColumnNamesCheckbox(true);
+}
+
+/**
+ * Sets the components related to the Excel worksheet columns.
+ * @param {JSON} columnsInfo - A JSON object containing information related to the spreadsheet
+ * columns.
+ * This information contains the names of the columns and column indices that contain HED tags.
+ */
+function setComponentsRelatedToColumns(columnsInfo) {
+    if (columnNamesAreEmpty(columnsInfo['column-names'])) {
+        setComponentsRelatedToEmptyColumnNames();
+    } else {
+        setComponentsRelatedToNonEmptyColumnNames(columnsInfo['column-names']);
+    }
+}
+
+/**
+ * Checks or unchecks the spreadsheet column names checkbox to false.
+ * @param {boolean} value  sets the checkbox to checked if true
+ */
+function setHasColumnNamesCheckbox(value) {
+    $('#has-column-names').prop('checked', value);
+}
+
+
+/**
+ * Submit the form and return the validation results. If there are issues then they are returned in an attachment
+ * file.
+ */
+function submitForm() {
+    let eventsForm = document.getElementById("events-form");
+    let formData = new FormData(eventsForm);
+    let worksheetName = $('#worksheet-name option:selected').text();
+    let prefix = 'issues';
+    if(worksheetName) {
+        prefix = prefix + '_worksheet_' + worksheetName;
+    }
+    let spreadsheetFile = $('#spreadsheet-file')[0].files[0].name;
+    let display_name = convertToResultsName(spreadsheetFile, prefix)
+    resetFormFlashMessages();
+    flashMessageOnScreen('Worksheet is being validated ...', 'success',
+        'events-validation-submit-flash')
+    $.ajax({
+            type: 'POST',
+            url: "{{url_for('route_blueprint.get_events_validation_results')}}",
+            data: formData,
+            contentType: false,
+            processData: false,
+            dataType: 'text',
+            success: function (download, status, xhr) {
+                getResponseSuccess(download, xhr, display_name, 'events-validation-submit-flash')
+            },
+            error: function (download, status, xhr) {
+                getResponseFailure(download, xhr, display_name, 'events-validation-submit-flash')
+            }
+        }
+    )
+}
\ No newline at end of file
diff --git a/hedweb/hed/web/templates/js/form-helpers.js b/hedweb/hed/web/templates/js/form-helpers.js
index f0f0b13f6..fb2a119ca 100644
--- a/hedweb/hed/web/templates/js/form-helpers.js
+++ b/hedweb/hed/web/templates/js/form-helpers.js
@@ -4,28 +4,44 @@
  * @param {String} filePath - A path to a file.
  * @returns {boolean} - True if the user selects cancel in chrome's file upload browser.
  */
-export function cancelWasPressedInChromeFileUpload(filePath) {
+function cancelWasPressedInChromeFileUpload(filePath) {
     return isEmptyStr(filePath) && (window.chrome)
 }
 
+/**
+ * Checks to see if any entries in an array of names are empty.
+ * @param {Array} names - An array containing a list of names.
+ * @returns {boolean} - True if any of the names in the array are all empty.
+ */
+function columnNamesAreEmpty(names) {
+    let numberOfNames = names.length;
+    for (let i = 0; i < numberOfNames; i++) {
+        if (!isEmptyStr(names[i].trim())) {
+            return false;
+        }
+    }
+    return true;
+}
+
 /**
  * Converts a path and prefix to a text results name
  * @param {String} filename - Pathname of the original file
  * @param {String} prefix - Prefix to be appended to the file name of original file
  * @returns {String} - File name of the
  */
-export function convertToResultsName(filename, prefix) {
+function convertToResultsName(filename, prefix) {
     let parts = getFilenameAndExtension(filename);
     return prefix + "_" + parts[0] + ".txt"
 }
 
+
 /**
  * Compares the file extension of the file at the specified path to an Array of accepted file extensions.
  * @param {String} filePath - A path to a file.
  * @param {Array} acceptedFileExtensions - An array of accepted file extensions.
  * @returns {boolean} - True if the file has an accepted file extension.
  */
-export function fileHasValidExtension(filePath, acceptedFileExtensions) {
+function fileHasValidExtension(filePath, acceptedFileExtensions) {
     let fileExtension = filePath.split('.').pop();
     if ($.inArray(fileExtension.toLowerCase(), acceptedFileExtensions) != -1) {
         return true;
@@ -34,18 +50,6 @@ export function fileHasValidExtension(filePath, acceptedFileExtensions) {
 }
 
 
-/**
- * Checks to see if a string is empty. Empty meaning null or a length of zero.
- * @param {String} str - A string.
- * @returns {boolean} - True if the string is null or its length is 0.
- */
-export function isEmptyStr(str) {
-    if (str === null || str.length === 0) {
-        return true;
-    }
-    return false;
-}
-
 
 /**
  * Flash a message on the screen.
@@ -53,14 +57,14 @@ export function isEmptyStr(str) {
  * @param {String} category - The category of the message. The categories are 'error', 'success', and 'other'.
  * @param {String} flashMessageElementId - ID of the flash element
  */
-export function flashMessageOnScreen(message, category, flashMessageElementId) {
+function flashMessageOnScreen(message, category, flashMessageElementId) {
     let flashMessage = document.getElementById(flashMessageElementId);
     flashMessage.innerHTML = message;
     setFlashMessageCategory(flashMessage, category);
 }
 
 
-export function getFilenameAndExtension(pathname){
+function getFilenameAndExtension(pathname){
 
   let clean = pathname.toString().replace(/^.*[\\\/]/, '');
   if (!clean) {
@@ -73,18 +77,94 @@ export function getFilenameAndExtension(pathname){
 }
 
 
+/**
+ * Gets the file download name from a Response header
+ * @param {Object} xhr - Dictionary containing Response header information
+ * @param {String} default_name - The default name to use
+ * @returns {String} - Name of the save file
+ */
+function getFilenameFromResponseHeader(xhr, default_name) {
+    let disposition = xhr.getResponseHeader('Content-Disposition')
+
+    let filename = "";
+    if (disposition && disposition.indexOf('attachment') !== -1) {
+        let filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
+        let matches = filenameRegex.exec(disposition);
+        if (matches != null && matches[1]) {
+            filename = matches[1].replace(/['"]/g, '');
+        }
+    }
+    if (!filename) {
+        filename = default_name;
+    }
+    return filename
+}
+
+/**
+ * Gets standard failure response for download
+ * @param {String} download - Downloaded string response blob
+ * @param {Object} xhr - Dictionary containing Response header information
+ * @param {String} display_name - Name used for the downloaded blob file
+ * @param {String} flash_location - ID of the flash location element for displaying response Message
+ */
+function getResponseFailure(download, xhr, display_name, flash_location) {
+    console.log(download.responseText);
+    if (download.responseText.length < 100) {
+        flashMessageOnScreen(download_response.responseText, 'error', flash_location);
+    } else {
+        flashMessageOnScreen('Not processed', 'error', flash_location);
+    }
+}
+
+/**
+ * Gets standard response with download
+ * @param {String} download - Downloaded string response blob
+ * @param {Object} xhr - Dictionary containing Response header information
+ * @param {String} display_name - Name used for the downloaded blob file
+ * @param {String} flash_location - ID of the flash location element for displaying response Message
+ */
+function getResponseSuccess(download, xhr, display_name, flash_location) {
+    if (download) {
+        let filename = getFilenameFromResponseHeader(xhr, display_name)
+        triggerDownloadBlob(download, filename);
+        let info = xhr.getResponseHeader('Message')
+        let category =  xhr.getResponseHeader('Category')
+        if (info) {
+            flashMessageOnScreen(info, category, flash_location);
+        } else {
+            flashMessageOnScreen('', 'success', flash_location);
+        }
+    } else {
+        flashMessageOnScreen('No errors found.', 'success', flash_location);
+    }
+}
+
+
+/**
+ * Checks to see if a string is empty. Empty meaning null or a length of zero.
+ * @param {String} str - A string.
+ * @returns {boolean} - True if the string is null or its length is 0.
+ */
+function isEmptyStr(str) {
+    if (str === null || str.length === 0) {
+        return true;
+    }
+    return false;
+}
+
+
 /**
  * Flash a message on the screen.
  * @param {Object} flashMessage - The li element containing the flash message.
  * @param {String} category - The category of the message. The categories are 'error', 'success', and 'other'.
  */
-export function setFlashMessageCategory(flashMessage, category) {
+function setFlashMessageCategory(flashMessage, category) {
     if ("error" === category) {
         flashMessage.style.backgroundColor = 'lightcoral';
     } else if ("success" === category) {
         flashMessage.style.backgroundColor = 'palegreen';
     } else if ("warning" === category) {
-        flashMessage.style.backgroundColor = 'darkorange';
+        flashMessage.style.backgroundColor = 'orange';
     } else {
         flashMessage.style.backgroundColor = '#f0f0f5';
     }
@@ -93,7 +173,7 @@ export function setFlashMessageCategory(flashMessage, category) {
 /**
  * Trigger the "save as" dialog for a text blob to save as a file with display name.
  */
-export function triggerDownloadBlob(download_text_blob, display_name) {
+function triggerDownloadBlob(download_text_blob, display_name) {
     const url = window.URL.createObjectURL(new Blob([download_text_blob]));
     const link = document.createElement('a');
     link.href = url;
@@ -103,10 +183,11 @@ export function triggerDownloadBlob(download_text_blob, display_name) {
 }
 
 /**
- * Updates the dictionary file label.
+ * Updates a file label.
  * @param {String} filePath - The path to the dictionary.
+ * @param {String} displayName - The ID of the label field to set
  */
-export function updateFileLabel(filePath, displayName) {
+function updateFileLabel(filePath, displayName) {
     let filename = filePath.split('\\').pop();
     $(displayName).text(filename);
 }
diff --git a/hedweb/hed/web/templates/js/hed-pulldown.js b/hedweb/hed/web/templates/js/hed-pulldown.js
index 6ef855bd3..3076af023 100644
--- a/hedweb/hed/web/templates/js/hed-pulldown.js
+++ b/hedweb/hed/web/templates/js/hed-pulldown.js
@@ -69,13 +69,13 @@ function getVersionFromHEDFile(hedXMLFile) {
     formData.append('hed-xml-file', hedXMLFile);
     $.ajax({
         type: 'POST',
-        url: "{{ url_for('route_blueprint.get_hed_version_in_file')}}",
+        url: "{{ url_for('route_blueprint.get_hed_version')}}",
         data: formData,
         contentType: false,
         processData: false,
         dataType: 'json',
         success: function (hedInfo) {
-            resetFlashMessages();
+            resetFormFlashMessages();
             flashMessageOnScreen('Using HED version ' + hedInfo['hed-version'],
                 'success', 'hed-flash');
         },
diff --git a/hedweb/hed/web/templates/js/json-input.js b/hedweb/hed/web/templates/js/json-input.js
index cddfbae39..32b81d568 100644
--- a/hedweb/hed/web/templates/js/json-input.js
+++ b/hedweb/hed/web/templates/js/json-input.js
@@ -5,7 +5,7 @@ const JSON_FILE_EXTENSIONS = ['json'];
  */
 $('#json-file').on('change',function () {
     let jsonPath = $('#json-file').val();
-    resetFlashMessages();
+    resetFormFlashMessages();
     if (cancelWasPressedInChromeFileUpload(jsonPath)) {
         resetForm();
     }
@@ -13,40 +13,35 @@ $('#json-file').on('change',function () {
         updateFileLabel(jsonPath, '#json-display-name');
     } else {
         resetForm();
-        flashMessageOnScreen('Please upload a JSON dictionary (.json)',
-            'error', 'json-flash');
+        flashMessageOnScreen('Please upload a JSON dictionary (.json)', 'error', 'json-flash');
     }
 });
 
-
 /**
  * Clears the dictionary file label.
  */
-export function clearJsonFileLabel() {
+function clearJsonFileLabel() {
     $('#json-display-name').text('');
 }
 
 /**
  * Resets the flash messages that aren't related to the form submission.
  */
-export function clearJsonFlashMessage() {
+function clearJsonFlashMessage() {
     flashMessageOnScreen('', 'success', 'json-flash');
 }
 
-
-export function getJsonFileLabel() {
+function getJsonFileLabel() {
     return $('#json-file')[0].files[0].name;
 }
 
-
 /**
  * Checks to see if a dictionary has been specified.
  */
-export function jsonIsSpecified() {
+function jsonIsSpecified() {
     let jsonFile = $('#json-file');
     if (jsonFile[0].files.length === 0) {
-        flashMessageOnScreen('JSON is not specified.', 'error',
-            'json-flash');
+        flashMessageOnScreen('JSON is not specified.', 'error', 'json-flash');
         return false;
     }
     return true;
diff --git a/hedweb/hed/web/templates/js/schema-form.js b/hedweb/hed/web/templates/js/schema-form.js
index b3dcafcaa..9eb884839 100644
--- a/hedweb/hed/web/templates/js/schema-form.js
+++ b/hedweb/hed/web/templates/js/schema-form.js
@@ -26,7 +26,7 @@ $('#schema-url').on('change', function () {
 /**
  * Submits the form for conversion if we have a valid file.
  */
-$('#schema-conversion-submit').click(function () {
+$('#schema-conversion-submit').on('click', function () {
     if (getSchemaFilename() === "") {
         flashMessageOnScreen('No valid source input file.  See above.', 'error',
             'schema-submit-flash')
@@ -64,9 +64,9 @@ function convertToOutputName(original_filename) {
     let basename = file_parts[0]
     let extension = file_parts[1]
     let new_extension = 'bad'
-    if (extension == SCHEMA_XML_EXTENSION) {
+    if (extension === SCHEMA_XML_EXTENSION) {
         new_extension = SCHEMA_MEDIAWIKI_EXTENSION
-    } else if (extension == SCHEMA_MEDIAWIKI_EXTENSION) {
+    } else if (extension === SCHEMA_MEDIAWIKI_EXTENSION) {
         new_extension = SCHEMA_XML_EXTENSION
     }
 
@@ -83,7 +83,7 @@ function getSchemaFilename() {
     let checkRadioVal = checkRadio.id
     let schemaFile = $('#schema-file');
     let schemaFileIsEmpty = schemaFile[0].files.length === 0;
-    if (checkRadioVal == "schema-file-option") {
+    if (checkRadioVal === "schema-file-option") {
         if (schemaFileIsEmpty) {
             flashMessageOnScreen('Schema file not specified.', 'error',
                 'schema-file-flash');
@@ -95,7 +95,7 @@ function getSchemaFilename() {
 
     let schemaUrl = $('#schema-url').val();
     let schemaUrlIsEmpty = schemaUrl === "";
-    if (checkRadioVal == "schema-url-option") {
+    if (checkRadioVal === "schema-url-option") {
         if (schemaUrlIsEmpty) {
             flashMessageOnScreen('URL not specified.', 'error', 'schema-url-flash');
             return '';
@@ -146,7 +146,7 @@ function splitExt(filename) {
 function submitSchemaConversionForm() {
     let schemaForm = document.getElementById("schema-form");
     let formData = new FormData(schemaForm);
-    let download_display_name = convertToOutputName(getSchemaFilename())
+    let display_name = convertToOutputName(getSchemaFilename())
     resetFlashMessages(false);
     flashMessageOnScreen('Specification is being converted...', 'success',
         'schema-submit-flash')
@@ -157,19 +157,11 @@ function submitSchemaConversionForm() {
             contentType: false,
             processData: false,
             dataType: "text",
-            success: function (downloaded_file) {
-                  flashMessageOnScreen('', 'success', 'schema-submit-flash');
-                  triggerDownloadBlob(downloaded_file, download_display_name);
+            success: function (download, status, xhr) {
+                getResponseSuccess(download, xhr, display_name, 'schema-submit-flash')
             },
-            error: function (download_response) {
-                console.log(download_response.responseText);
-                if (download_response.responseText.length < 100) {
-                    flashMessageOnScreen(download_response.responseText, 'error',
-                        'schema-submit-flash');
-                } else {
-                    flashMessageOnScreen('XML could not be processed', 'error',
-                        'schema-submit-flash');
-                }
+            error: function (download, status, xhr) {
+                getResponseFailure(download, xhr, display_name, 'schema-submit-flash')
             }
         }
     )
@@ -179,7 +171,7 @@ function submitSchemaConversionForm() {
 function submitSchemaComplianceCheckForm() {
     let schemaForm = document.getElementById("schema-form");
     let formData = new FormData(schemaForm);
-    let download_display_name = convertToResultsName(getSchemaFilename(), 'HED3G_format_issues')
+    let display_name = convertToResultsName(getSchemaFilename(), 'HED3G_format_issues')
     resetFlashMessages(false);
     flashMessageOnScreen('Checking schema for HED-3G compliance...', 'success',
         'schema-submit-flash')
@@ -190,28 +182,14 @@ function submitSchemaComplianceCheckForm() {
             contentType: false,
             processData: false,
             dataType: 'text',
-            success: function (downloaded_file) {
-                  if (downloaded_file) {
-                      flashMessageOnScreen('', 'success', 'schema-submit-flash');
-                      triggerDownloadBlob(downloaded_file, download_display_name);
-                  } else {
-                      flashMessageOnScreen('No HED-3G compliance issues found.', 'success',
-                          'schema-submit-flash');
-                  }
+            success: function (download, status, xhr) {
+                getResponseSuccess(download, xhr, display_name, 'schema-submit-flash')
             },
-            error: function (download_response) {
-                console.log(download_response.responseText);
-                if (download_response.responseText.length < 100) {
-                    flashMessageOnScreen(download_response.responseText, 'error',
-                        'schema-submit-flash');
-                } else {
-                    flashMessageOnScreen('Schema could not be processed',
-                        'error','schema-submit-flash');
-                }
+            error: function (download, status, xhr) {
+                getResponseFailure(download, xhr, display_name, 'schema-submit-flash')
             }
         }
     )
-    ;
 }
 
 function updateFormGui() {
@@ -256,24 +234,6 @@ function updateFormGui() {
      flashMessageOnScreen('', 'success', 'schema-submit-flash')
 }
 
-/**
- * Updates the HED file label.
- * @param {String} hedPath - The path to the HED XML file.
- */
-function updateSchemaFileLabel(hedPath) {
-    let hedFilename = hedPath.split('\\').pop();
-    $('#schema-file-display-name').text(hedFilename);
-}
-
-/**
- * Updates the HED file label.
- * @param {String} hedPath - The path to the HED XML file.
- */
-function updateSchemaUrlLabel(hedPath) {
-    let hedFilename = hedPath.split('\\').pop();
-    $('#schema-url-display-name').text(hedFilename);
-}
-
 
 function urlFileBasename(url) {
     let urlObj = null
diff --git a/hedweb/hed/web/templates/js/spreadsheet-form.js b/hedweb/hed/web/templates/js/spreadsheet-form.js
index 22bfdda53..394fbd7fb 100644
--- a/hedweb/hed/web/templates/js/spreadsheet-form.js
+++ b/hedweb/hed/web/templates/js/spreadsheet-form.js
@@ -1,35 +1,6 @@
-const EXCEL_FILE_EXTENSIONS = ['xlsx', 'xls'];
-const TEXT_FILE_EXTENSIONS = ['tsv', 'txt'];
 
 $(function () {
-    prepareSpreadsheetForm();
-});
-
-
-/**
- * Spreadsheet event handler function. Checks if the file uploaded has a valid spreadsheet extension.
- */
-$('#spreadsheet-file').on('change', function () {
-    let spreadsheet = $('#spreadsheet-file');
-    let spreadsheetPath = spreadsheet.val();
-    let spreadsheetFile = spreadsheet[0].files[0];
-    resetFlashMessages();
-    if (cancelWasPressedInChromeFileUpload(spreadsheetPath)) {
-        resetForm();
-    }
-    else if (fileHasValidExtension(spreadsheetPath, EXCEL_FILE_EXTENSIONS)) {
-        updateFileLabel(spreadsheetPath, '#spreadsheet-display-name');
-        getWorksheetsInfo(spreadsheetFile);
-    }
-    else if (fileHasValidExtension(spreadsheetPath, TEXT_FILE_EXTENSIONS)) {
-        updateFileLabel(spreadsheetPath, '#spreadsheet-display-name');
-        clearWorksheetSelectbox();
-        getColumnsInfo(spreadsheetFile, '');
-    } else {
-        resetForm();
-        flashMessageOnScreen('Please upload a excel or text spreadsheet (.xlsx, .xls, .tsv, .txt)',
-            'error', 'spreadsheet-flash');
-    }
+    prepareForm();
 });
 
 /**
@@ -41,23 +12,6 @@ $('#spreadsheet-validation-submit').on('click', function () {
     }
 });
 
-/**
- * Gets the information associated with the Excel worksheet that was newly selected. This information contains
- * the names of the columns and column indices that contain HED tags.
- */
-$('#worksheet-name').on('change', function () {
-    let spreadsheetFile = $('#spreadsheet-file')[0].files[0];
-    let worksheetName = $('#worksheet-name option:selected').text();
-    resetFlashMessages();
-    getColumnsInfo(spreadsheetFile, worksheetName);
-});
-
-/**
- * Clears the spreadsheet file label.
- */
-function clearSpreadsheetFileLabel() {
-    $('#spreadsheet-display-name').text('');
-}
 
 /**
  * Clears tag column text boxes.
@@ -66,22 +20,6 @@ function clearTagColumnTextboxes() {
     $('.textbox-group input[type="text"]').val('');
 }
 
-/**
- * Clears the worksheet select box.
- */
-function clearWorksheetSelectbox() {
-    $('#worksheet-name').empty();
-}
-
-/**
- * Checks to see if a dictionary is empty
- * @param {Array} dictionary - A dictionary
- * @returns {boolean} - True if the dictionary is empty. False, if otherwise.
- */
-function dictionaryIsEmpty(dictionary) {
-    return Object.keys(dictionary).length === 0;
-}
-
 /**
  * Flash a message showing the number of column columns that contain tags.
  * @param {Array} tagColumnIndices - An array of indices of columns containing tags
@@ -99,16 +37,6 @@ function flashTagColumnCountMessage(tagColumnIndices, requiredTagColumnIndices)
     }
 }
 
-/**
- * Flash a message showing the number of worksheets in an Excel workbook.
- * @param {Array} worksheetNames - An array containing the names of Excel workbook worksheets.
- */
-function flashWorksheetNumberMessage(worksheetNames) {
-    let numberOfWorksheets = worksheetNames.length.toString();
-    flashMessageOnScreen(numberOfWorksheets + ' worksheet(s) found',
-        'success', 'worksheet-flash');
-}
-
 /**
  * Gets the spreadsheet columns.
  * @param {Object} spreadsheetFile - A spreadsheet file.
@@ -172,29 +100,6 @@ function getWorksheetsInfo(workbookFile) {
     });
 }
 
-/**
- * Hides  columns section in the form.
- */
-function hideColumnNamesTable() {
-    $('#column-names').hide();
-}
-
-
-/**
- * Populates a table containing the worksheet columns.
- * @param {Array} columnNames - An array containing the spreadsheet column names.
- */
-function populateColumnNamesTable(columnNames) {
-    let columnNamesTable = $('#columns-names-table');
-    let columnNamesRow = $('<tr/>');
-    let numberOfColumnNames = columnNames.length;
-    columnNamesTable.empty();
-    for (let i = 0; i < numberOfColumnNames; i++) {
-        columnNamesRow.append('<td>' + columnNames[i] + '</td>');
-    }
-    columnNamesTable.append(columnNamesRow);
-}
-
 
 /**
  * Populate the required tag column textboxes from the tag column indices found in the spreadsheet columns.
@@ -216,37 +121,25 @@ function populateTagColumnsTextbox(tagColumnIndices) {
     $('#tag-columns').val(tagColumnIndices.sort().map(String));
 }
 
-/**
- * Populate the Excel worksheet select box.
- * @param {Array} worksheetNames - An array containing the Excel worksheet names.
- */
-function populateWorksheetSelectbox(worksheetNames) {
-    let worksheetSelectbox = $('#worksheet-name');
-    let numberOfWorksheetNames = worksheetNames.length;
-    worksheetSelectbox.empty();
-    for (let i = 0; i < numberOfWorksheetNames; i++) {
-        worksheetSelectbox.append(new Option(worksheetNames[i], worksheetNames[i]));
-    }
-}
 
 /**
  * Prepare the validation form after the page is ready. The form will be reset to handle page refresh and
  * components will be hidden and populated.
  */
-function prepareSpreadsheetForm() {
+function prepareForm() {
     resetForm();
     getHEDVersions()
-    hideColumnNamesTable();
+    hideColumnNames();
     hideOtherHEDVersionFileUpload();
 }
 
 /**
  * Resets the flash messages that aren't related to the form submission.
  */
-function resetFlashMessages() {
+function resetFormFlashMessages() {
     flashMessageOnScreen('', 'success', 'spreadsheet-flash');
     flashMessageOnScreen('', 'success', 'worksheet-flash');
-    flashMessageOnScreen('', 'success', 'tag-columns-flash');
+    resetTagColumnMessages();
     flashMessageOnScreen('', 'success', 'hed-flash');
     flashMessageOnScreen('', 'success', 'spreadsheet-validation-submit-flash');
 }
@@ -258,17 +151,24 @@ function resetForm() {
     $('#spreadsheet-form')[0].reset();
     clearSpreadsheetFileLabel();
     clearWorksheetSelectbox();
-    hideColumnNamesTable();
+    hideColumnNames();
     hideOtherHEDVersionFileUpload()
 }
 
+/**
+ * Resets the flash messages that aren't related to the form submission.
+ */
+function resetTagColumnMessages() {
+    flashMessageOnScreen('', 'success', 'tag-columns-flash');
+}
+
 /**
  * Sets the components related to Excel worksheet columns when they are all empty.
  */
 function setComponentsRelatedToEmptyColumnNames() {
     clearTagColumnTextboxes();
-    setHasColumnNamesCheckboxToFalse();
-    hideColumnNamesTable();
+    setHasColumnNamesCheckbox(false);
+    hideColumnNames();
 }
 
 /**
@@ -283,9 +183,8 @@ function setComponentsRelatedToEmptyTagColumnIndices() {
  * @param {Array} columnNames - An array containing the spreadsheet column names.
  */
 function setComponentsRelatedToNonEmptyColumnNames(columnNames) {
-    populateColumnNamesTable(columnNames);
-    setHasColumnNamesCheckboxToTrue();
-    $('#column-names').show();
+    showColumnNames(columnNames)
+    setHasColumnNamesCheckbox(true);
 }
 
 /**
@@ -306,51 +205,17 @@ function setComponentsRelatedToColumns(columnsInfo) {
     } else {
         populateTagColumnsTextbox(columnsInfo['tag-column-indices']);
     }
-    if (!dictionaryIsEmpty(columnsInfo['required-tag-column-indices'])) {
+    if (Object.keys(columnsInfo['required-tag-column-indices']).length !== 0) {
         populateRequiredTagColumnTextboxes(columnsInfo['required-tag-column-indices']);
     }
 }
 
 /**
  * Sets the spreadsheet has column names checkbox to false.
+ * @param {boolean} Box is checked if true and unchecked if false
  */
-function setHasColumnNamesCheckboxToFalse() {
-    $('#has-column-names').prop('checked', false);
-}
-
-/**
- * Sets the spreadsheet has column names checkbox to true.
- */
-function setHasColumnNamesCheckboxToTrue() {
-    $('#has-column-names').prop('checked', true);
-}
-
-/**
- * Checks to see if the spreadsheet columns are empty.
- * @param {Array} columnNames - An array containing the spreadsheet column names.
- * @returns {boolean} - True if the spreadsheet columns are all empty.
- */
-function columnNamesAreEmpty(columnNames) {
-    let numberOfColumnNames = columnNames.length;
-    for (let i = 0; i < numberOfColumnNames; i++) {
-        if (!isEmptyStr(columnNames[i].trim())) {
-            return false;
-        }
-    }
-    return true;
-}
-
-/**
- * Checks to see if a spreadsheet has been specified.
- */
-function spreadsheetIsSpecified() {
-    let spreadsheetFile = $('#spreadsheet-file');
-    if (spreadsheetFile[0].files.length === 0) {
-        flashMessageOnScreen('Spreadsheet is not specified.', 'error',
-            'spreadsheet-flash');
-        return false;
-    }
-    return true;
+function setHasColumnNamesCheckbox(value) {
+    $('#has-column-names').prop('checked', value);
 }
 
 /**
@@ -374,13 +239,13 @@ function submitForm() {
     let spreadsheetForm = document.getElementById("spreadsheet-form");
     let formData = new FormData(spreadsheetForm);
     let worksheetName = $('#worksheet-name option:selected').text();
-    let prefix = 'validation_issues';
+    let prefix = 'issues';
     if(worksheetName) {
         prefix = prefix + '_worksheet_' + worksheetName;
     }
     let spreadsheetFile = $('#spreadsheet-file')[0].files[0].name;
     let display_name = convertToResultsName(spreadsheetFile, prefix)
-    resetFlashMessages();
+    resetFormFlashMessages();
     flashMessageOnScreen('Worksheet is being validated ...', 'success',
         'spreadsheet-validation-submit-flash')
     $.ajax({
@@ -390,29 +255,14 @@ function submitForm() {
             contentType: false,
             processData: false,
             dataType: 'text',
-            success: function (downloaded_file) {
-                  if (downloaded_file) {
-                      flashMessageOnScreen('', 'success',
-                          'spreadsheet-validation-submit-flash');
-                      triggerDownloadBlob(downloaded_file, display_name);
-                  } else {
-                      flashMessageOnScreen('No validation errors found.', 'success',
-                          'spreadsheet-validation-submit-flash');
-                  }
+            success: function (download, status, xhr) {
+                getResponseSuccess(download, xhr, display_name, 'spreadsheet-validation-submit-flash')
             },
-            error: function (download_response) {
-                console.log(download_response.responseText);
-                if (download_response.responseText.length < 100) {
-                    flashMessageOnScreen(download_response.responseText, 'error',
-                        'spreadsheet-validation-submit-flash');
-                } else {
-                    flashMessageOnScreen('Spreadsheet could not be processed',
-                        'error','spreadsheet-validation-submit-flash');
-                }
+            error: function (download, status, xhr) {
+                getResponseFailure(download, xhr, display_name, 'spreadsheet-validation-submit-flash')
             }
         }
     )
-    ;
 }
 
 /**
diff --git a/hedweb/hed/web/templates/js/spreadsheet-input.js b/hedweb/hed/web/templates/js/spreadsheet-input.js
new file mode 100644
index 000000000..18e97ce12
--- /dev/null
+++ b/hedweb/hed/web/templates/js/spreadsheet-input.js
@@ -0,0 +1,136 @@
+/**
+ * This code handles the input of a spreadsheet name and worksheet names and displays table headers. It has
+ * the form handling for spreadsheet-input.html.
+ * @type {string[]}
+ */
+const EXCEL_FILE_EXTENSIONS = ['xlsx', 'xls'];
+const TEXT_FILE_EXTENSIONS = ['tsv', 'txt'];
+const spreadsheetInputIDs = ['spreadsheet-file', 'spreadsheet-display-name', 'spreadsheet-flash',
+                             'worksheet-name', 'columns-names-table']
+
+/**
+ * Spreadsheet event handler function. Checks if the file uploaded has a valid spreadsheet extension.
+ */
+$('#spreadsheet-file').on('change', function () {
+    let spreadsheet = $('#spreadsheet-file');
+    let spreadsheetPath = spreadsheet.val();
+    let spreadsheetFile = spreadsheet[0].files[0];
+    resetFormFlashMessages();
+    if (cancelWasPressedInChromeFileUpload(spreadsheetPath)) {
+        resetForm();
+    }
+    else if (fileHasValidExtension(spreadsheetPath, EXCEL_FILE_EXTENSIONS)) {
+        updateFileLabel(spreadsheetPath, '#spreadsheet-display-name');
+        getWorksheetsInfo(spreadsheetFile);
+    }
+    else if (fileHasValidExtension(spreadsheetPath, TEXT_FILE_EXTENSIONS)) {
+        updateFileLabel(spreadsheetPath, '#spreadsheet-display-name');
+        clearWorksheetSelectbox();
+        getColumnsInfo(spreadsheetFile, '');
+    } else {
+        resetForm();
+        flashMessageOnScreen('Please upload a excel or text spreadsheet (.xlsx, .xls, .tsv, .txt)',
+            'error', 'spreadsheet-flash');
+    }
+});
+
+
+/**
+ * Gets the information associated with the Excel worksheet that was newly selected. This information contains
+ * the names of the columns and column indices that contain HED tags.
+ */
+$('#worksheet-name').on('change', function () {
+    let spreadsheetFile = $('#spreadsheet-file')[0].files[0];
+    let worksheetName = $('#worksheet-name option:selected').text();
+    resetFormFlashMessages();
+    getColumnsInfo(spreadsheetFile, worksheetName);
+});
+
+/**
+ * Clears the spreadsheet file label.
+ */
+function clearSpreadsheetFileLabel() {
+    $('#spreadsheet-display-name').text('');
+}
+
+/**
+ * Clears the worksheet select box.
+ */
+function clearWorksheetSelectbox() {
+    $('#worksheet-name').empty();
+}
+
+/**
+ * Flash a message showing the number of worksheets in an Excel workbook.
+ * @param {Array} worksheetNames - An array containing the names of Excel workbook worksheets.
+ */
+function flashWorksheetNumberMessage(worksheetNames) {
+    let numberOfWorksheets = worksheetNames.length.toString();
+    flashMessageOnScreen(numberOfWorksheets + ' worksheet(s) found',
+        'success', 'worksheet-flash');
+}
+
+/**
+ * Hides  columns section in the form.
+ */
+function hideColumnNames() {
+    $('#column-names').hide();
+}
+
+/**
+ * Populates a table containing one row.
+ * @param {Array} columnNames - An array containing the spreadsheet column names.
+ * @param {String} tableID - String containing the ID of the table
+ */
+function populateTableHeaders(columnNames, tableID) {
+    let columnTable = $('#columns-names-table');
+    let columnNamesRow = $('<tr/>');
+    let numberOfColumnNames = columnNames.length;
+    columnTable.empty();
+    for (let i = 0; i < numberOfColumnNames; i++) {
+        columnNamesRow.append('<td>' + columnNames[i] + '</td>');
+    }
+    columnTable.append(columnNamesRow);
+}
+
+/**
+ * Populate the Excel worksheet select box.
+ * @param {Array} worksheetNames - An array containing the Excel worksheet names.
+ */
+function populateWorksheetSelectbox(worksheetNames) {
+    let worksheetSelectbox = $('#worksheet-name');
+    let numberOfWorksheetNames = worksheetNames.length;
+    worksheetSelectbox.empty();
+    for (let i = 0; i < numberOfWorksheetNames; i++) {
+        worksheetSelectbox.append(new Option(worksheetNames[i], worksheetNames[i]));
+    }
+}
+
+/**
+ * Resets the flash messages that aren't related to the form submission.
+ */
+function resetSpreadsheetFlashMessage() {
+    flashMessageOnScreen('', 'success', 'spreadsheet-flash');
+}
+
+/**
+ * Sets the components related to the spreadsheet columns when they are not empty.
+ * @param {Array} columnNames - An array containing the spreadsheet column names.
+ */
+function showColumnNames(columnNames) {
+    populateTableHeaders(columnNames);
+    $('#column-names').show();
+}
+
+/**
+ * Checks to see if a spreadsheet has been specified.
+ */
+function spreadsheetIsSpecified() {
+    let spreadsheetFile = $('#spreadsheet-file');
+    if (spreadsheetFile[0].files.length === 0) {
+        flashMessageOnScreen('Spreadsheet is not specified.', 'error',
+            'spreadsheet-flash');
+        return false;
+    }
+    return true;
+}
\ No newline at end of file
diff --git a/hedweb/hed/web/templates/json-input.html b/hedweb/hed/web/templates/json-input.html
index 1478d2d76..31fbe849e 100644
--- a/hedweb/hed/web/templates/json-input.html
+++ b/hedweb/hed/web/templates/json-input.html
@@ -1,10 +1,10 @@
 {% macro create_json_input() %}
         <div class="form-group">
             <label for="json-file">Select JSON-formatted dictionary or side-car:</label><br>
-            <label class="btn-group file-browse-group json-btn-group">
+            <label class="btn-group file-browse-group">
                 <button for="json-file">Browse ...</button>
                 <label id="json-display-name"></label>
-                <input type="file" name="json-file" id="json-file" multiple/>
+                <input type="file" name="json-file" id="json-file"/>
             </label>
             <p class="flash" id="json-flash"></p>
         </div>
diff --git a/hedweb/hed/web/templates/layout.html b/hedweb/hed/web/templates/layout.html
index 9d1e3d589..d0fee1655 100644
--- a/hedweb/hed/web/templates/layout.html
+++ b/hedweb/hed/web/templates/layout.html
@@ -1,6 +1,8 @@
 <!doctype html>
-<html>
+<html lang="en">
 <head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
     <!-- Latest compiled and minified CSS -->
     <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
           integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
@@ -11,7 +13,7 @@
           crossorigin="anonymous">
     <link rel="shortcut icon" href="{{ url_for('static', filename='img/favicon.ico') }}">
     <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/layout.css') }}">
-    <script src="//ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
+    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
     {% block head %}
     {% endblock %}
     <title>
@@ -25,10 +27,11 @@
     <h1 class="project-name">Hierarchical Event Descriptor (HED) online tools</h1>
     <h2 class="project-tagline">Online tools for HED schema, HED tags, and event files</h2>
     <a href="{{ url_for('route_blueprint.render_home_page') }}" class="btn">HED tools online</a>
-    <a href="{{ url_for('route_blueprint.render_spreadsheet_validation_form') }}" class="btn">Validate spreadsheet</a>
-    <a href="{{ url_for('route_blueprint.render_dictionary_validation_form') }}" class="btn">Validate dictionary</a>
+     <a href="{{ url_for('route_blueprint.render_events_form') }}" class="btn">Validate events</a>
+    <a href="{{ url_for('route_blueprint.render_spreadsheet_form') }}" class="btn">Validate spreadsheet</a>
+    <a href="{{ url_for('route_blueprint.render_dictionary_form') }}" class="btn">Validate dictionary</a>
     <a href="{{ url_for('route_blueprint.render_schema_form') }}" class="btn">Process schema</a>
-    <a href="{{ url_for('route_blueprint.render_help_page') }}" class="btn">HED tools help</a>
+   <!-- <a href="{{ url_for('route_blueprint.render_help_page') }}" class="btn">HED tools help</a> -->
     <a href="https://github.com/hed-standard" class="btn">HED standard</a>
 </section>
 <section class="main-content">
@@ -47,7 +50,7 @@ <h2 class="project-tagline">Online tools for HED schema, HED tags, and event fil
         integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
         crossorigin="anonymous"></script>
 <script type="text/javascript">
-    var csrf_token = "{{ csrf_token() }}";
+    let csrf_token = "{{ csrf_token() }}";
     $.ajaxSetup({
         beforeSend: function (xhr, settings) {
             if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
diff --git a/hedweb/hed/web/templates/schema.html b/hedweb/hed/web/templates/schema.html
index 42d83d5d7..560b4c9b7 100644
--- a/hedweb/hed/web/templates/schema.html
+++ b/hedweb/hed/web/templates/schema.html
@@ -4,29 +4,31 @@ <h2>Process HED Schema</h2>
     <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/schema-form.css') }}">
     <form id="schema-form" method="POST" enctype="multipart/form-data">
         <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
-        <h3>Source file (.xml/.mediawiki): </h3>
+        <h3>Select a schema source file (.xml/.mediawiki): </h3>
         <div class="form-group" id="schema-url-group">
-            <input class="radio-btn" type="radio" name="schema-upload-options" id="schema-url-option" value="schema-url-option">
-            <label class="secondary-label" for="schema-url-version">Enter HED schema URL:</label>
+            <input class="radio-btn" type="radio" name="schema-upload-options"
+                   id="schema-url-option" value="schema-url-option" aria-label="Select this button for HED schema URL">
+            <label class="secondary-label">Enter HED schema URL:</label>
             <label class="btn-group url-browse-group">
                 <label id="schema-url-display-name"></label>
-                <input type="url" name="schema-url" id="schema-url">
+                <input type="url" name="schema-url" id="schema-url" aria-label="Enter schema URL here">
             </label>
             <p class="flash" id="schema-url-flash"></p>
         </div>
 
         <div class="form-group" id="schema-file-group">
-            <input class="radio-btn" type="radio" name="schema-upload-options" id="schema-file-option" value="schema-file-option">
+            <input class="radio-btn" type="radio" name="schema-upload-options" id="schema-file-option"
+                   value="schema-file-option" aria-label="Select this button for HED schema file">
             <label class="secondary-label" for="schema-file">Upload HED schema file:</label>
             <label class="btn-group file-browse-group">
-                <button for="schema-file">Browse ...</button>
+                <button aria-label="Button for uploading HED file">Browse ...</button>
                 <label id="schema-file-display-name"></label>
-                <input type="file" name="schema-file" id="schema-file"/>
+                <input type="file" name="schema-file" id="schema-file" aria-label="Enter schema file to upload here"/>
             </label>
             <p class="flash" id="schema-file-flash"></p>
         </div>
 
-        <h3>Actions (pick one): </h3>
+        <h3>Pick an action: </h3>
         <div class="form-group">
             <button id="schema-conversion-submit" type="button">Convert schema format</button>
         </div>
diff --git a/hedweb/hed/web/templates/spreadsheet-input.html b/hedweb/hed/web/templates/spreadsheet-input.html
new file mode 100644
index 000000000..67bbb8d3d
--- /dev/null
+++ b/hedweb/hed/web/templates/spreadsheet-input.html
@@ -0,0 +1,23 @@
+{% macro spreadsheet_input() %}
+        <div class="form-group">
+            <label for="spreadsheet-file">Select spreadsheet (Excel or tab-separated):</label><br>
+            <label class="btn-group file-browse-group">
+                <button for="spreadsheet-file">Browse ...</button>
+                <label id="spreadsheet-display-name"></label>
+                <input type="file" name="spreadsheet-file" id="spreadsheet-file"/>
+            </label>
+            <p class="flash" id="spreadsheet-flash"></p>
+        </div>
+
+        <div class="form-group">
+            <label for="worksheet-name">Choose a worksheet (for workbooks):</label><br>
+            <select name="worksheet-name" id="worksheet-name"></select>
+            <p class="flash" id="worksheet-flash"></p>
+        </div>
+
+        <div class="form-group" id="column-names">
+            <label class="secondary-label" for="columns-names-table">Worksheet columns</label>
+            <table id="columns-names-table"></table>
+        </div>
+
+{% endmacro %}
\ No newline at end of file
diff --git a/hedweb/hed/web/templates/spreadsheet.html b/hedweb/hed/web/templates/spreadsheet.html
index ef2311cf7..af59e60fd 100644
--- a/hedweb/hed/web/templates/spreadsheet.html
+++ b/hedweb/hed/web/templates/spreadsheet.html
@@ -1,31 +1,13 @@
 {% extends "layout.html" %}
 {% from "hed-pulldown.html" import create_hed_pulldown %}
+{% from "spreadsheet-input.html" import spreadsheet_input %}
 {% block content %}
     <h2>Validate spreadsheet containing HED tags</h2>
-    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/spreadsheet-form.css') }}">
+    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/forms.css') }}">
     <form id="spreadsheet-form" method="POST" enctype="multipart/form-data">
         <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
 
-        <div class="form-group">
-            <label for="spreadsheet-file">Select spreadsheet (Excel or tab-separated):</label><br>
-            <label class="btn-group file-browse-group spreadsheet-btn-group">
-                <button for="spreadsheet-file">Browse ...</button>
-                <label id="spreadsheet-display-name"></label>
-                <input type="file" name="spreadsheet-file" id="spreadsheet-file"/>
-            </label>
-            <p class="flash" id="spreadsheet-flash"></p>
-        </div>
-
-        <div class="form-group">
-            <label for="worksheet-name">Choose a worksheet (for workbooks):</label><br>
-            <select name="worksheet-name" id="worksheet-name"></select>
-            <p class="flash" id="worksheet-flash"></p>
-        </div>
-
-        <div class="form-group" id="column-names">
-            <label class="secondary-label" for="columns-names-table">Worksheet columns</label>
-            <table id="columns-names-table"></table>
-        </div>
+        {{ spreadsheet_input() }}
 
         <div class="form-group">
             <label>Special non-prefixed tag columns (if available):</label><br>
@@ -69,6 +51,7 @@ <h2>Validate spreadsheet containing HED tags</h2>
     <script type="module">
         {%  include 'js/form-helpers.js' %}
         {%  include 'js/hed-pulldown.js' %}
+        {%  include 'js/spreadsheet-input.js' %}
         {%  include 'js/spreadsheet-form.js' %}
     </script>
 
diff --git a/hedweb/hed/web/utils.py b/hedweb/hed/web/utils.py
index 3a0f06e73..7470cef53 100644
--- a/hedweb/hed/web/utils.py
+++ b/hedweb/hed/web/utils.py
@@ -3,7 +3,7 @@
 
 from flask import current_app
 
-from hed.util.file_util import get_file_extension, delete_file_if_it_exist
+from hed.util.file_util import get_file_extension, delete_file_if_it_exists
 
 from hed.web.constants import common_constants, error_constants, spreadsheet_constants
 from hed.web.web_utils import save_file_to_upload_folder, find_all_str_indices_in_list
@@ -11,12 +11,12 @@
 app_config = current_app.config
 
 
-def find_spreadsheet_columns_info(form_request_object):
+def find_spreadsheet_columns_info(request):
     """Finds the info associated with the spreadsheet columns.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from the validation form.
 
     Returns
@@ -28,11 +28,11 @@ def find_spreadsheet_columns_info(form_request_object):
     spreadsheet_columns_info = []
     try:
         spreadsheet_columns_info = {common_constants.COLUMN_NAMES: [], common_constants.TAG_COLUMN_INDICES: []}
-        if common_constants.SPREADSHEET_FILE in form_request_object.files:
-            spreadsheet_file = form_request_object.files[common_constants.SPREADSHEET_FILE]
+        if common_constants.SPREADSHEET_FILE in request.files:
+            spreadsheet_file = request.files[common_constants.SPREADSHEET_FILE]
             spreadsheet_file_path = save_file_to_upload_folder(spreadsheet_file)
-            if spreadsheet_file_path and worksheet_name_present_in_form(form_request_object):
-                worksheet_name = form_request_object.form[common_constants.WORKSHEET_NAME]
+            if spreadsheet_file_path and worksheet_name_present_in_form(request):
+                worksheet_name = request.form[common_constants.WORKSHEET_NAME]
                 spreadsheet_columns_info = populate_spreadsheet_columns_info_dictionary(spreadsheet_columns_info,
                                                                                         spreadsheet_file_path,
                                                                                         worksheet_name)
@@ -42,7 +42,7 @@ def find_spreadsheet_columns_info(form_request_object):
     except:
         spreadsheet_columns_info[error_constants.ERROR_KEY] = traceback.format_exc()
     finally:
-        delete_file_if_it_exist(spreadsheet_file_path)
+        delete_file_if_it_exists(spreadsheet_file_path)
     return spreadsheet_columns_info
 
 
@@ -86,41 +86,12 @@ def get_excel_worksheet_names(workbook_file_path):
     return worksheet_names
 
 
-def get_optional_form_field(form_request_object, form_field_name, type=''):
-    """Gets the specified optional form field if present.
-
-    Parameters
-    ----------
-    form_request_object: Request object
-        A Request object containing user data from the validation form.
-    form_field_name: string
-        The name of the optional form field.
-    type: str
-        Name of expected type: 'boolean' or 'string'
-
-    Returns
-    -------
-    boolean or string
-        A boolean or string value based on the form field type.
-
-    """
-    form_field_value = ''
-    if type == common_constants.BOOLEAN:
-        form_field_value = False
-        if form_field_name in form_request_object.form:
-            form_field_value = True
-    elif type == common_constants.STRING:
-        if form_field_name in form_request_object.form:
-            form_field_value = form_request_object.form[form_field_name]
-    return form_field_value
-
-
-def get_specific_tag_columns_from_form(form_request_object):
+def get_specific_tag_columns_from_form(request):
     """Gets the specific tag columns from the validation form.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from the validation form.
 
     Returns
@@ -132,8 +103,8 @@ def get_specific_tag_columns_from_form(form_request_object):
     column_prefix_dictionary = {}
     for tag_column_name in spreadsheet_constants.SPECIFIC_TAG_COLUMN_NAMES:
         form_tag_column_name = tag_column_name.lower() + common_constants.COLUMN_POSTFIX
-        if form_tag_column_name in form_request_object.form:
-            tag_column_name_index = form_request_object.form[form_tag_column_name].strip()
+        if form_tag_column_name in request.form:
+            tag_column_name_index = request.form[form_tag_column_name].strip()
             if tag_column_name_index:
                 tag_column_name_index = int(tag_column_name_index)
 
@@ -302,18 +273,18 @@ def populate_worksheets_info_dictionary(worksheets_info, spreadsheet_file_path):
     return worksheets_info
 
 
-def worksheet_name_present_in_form(validation_form_request_object):
-    """Checks to see if a worksheet name is present in a request object from the validation form.
+def worksheet_name_present_in_form(request):
+    """Checks to see if a worksheet name is present in a request object from form.
 
     Parameters
     ----------
-    validation_form_request_object: Request object
-        A Request object containing user data from the validation form.
+    request: Request object
+        A Request object containing user data from a form.
 
     Returns
     -------
     boolean
-        True if a worksheet name is present in a request object from the validation form.
+        True if a worksheet name is present in a request object from the form.
 
     """
-    return common_constants.WORKSHEET_NAME in validation_form_request_object.form
+    return common_constants.WORKSHEET_NAME in request.form
diff --git a/hedweb/hed/web/web_utils.py b/hedweb/hed/web/web_utils.py
index c89b57e29..0990c30d1 100644
--- a/hedweb/hed/web/web_utils.py
+++ b/hedweb/hed/web/web_utils.py
@@ -3,10 +3,12 @@
 import tempfile
 import traceback
 from urllib.parse import urlparse
+from werkzeug.utils import secure_filename
 from flask import current_app, jsonify, Response
 
 from hed.util import hed_cache
-from hed.util.file_util import get_file_extension, delete_file_if_it_exist
+from hed.util.error_reporter import ErrorHandler
+from hed.util.file_util import get_file_extension, delete_file_if_it_exists
 from hed.util.hed_schema import HedSchema
 from hed.web.constants import common_constants, error_constants
 
@@ -74,15 +76,15 @@ def find_all_str_indices_in_list(list_of_str, str_value):
             value.lower().replace(' ', '') == str_value.lower().replace(' ', '')]
 
 
-def find_hed_version_in_uploaded_file(form_request_object, key_name=common_constants.HED_XML_FILE):
+def find_hed_version_in_uploaded_file(request, key_name=common_constants.HED_XML_FILE):
     """Finds the version number in an HED XML other.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data
     key_name: str
-        Name of the key for the HED XML file in the form_request_object
+        Name of the key for the HED XML file in the request
 
     Returns
     -------
@@ -92,20 +94,20 @@ def find_hed_version_in_uploaded_file(form_request_object, key_name=common_const
     """
     hed_info = {}
     try:
-        if key_name in form_request_object.files:
-            hed_file_path = save_file_to_upload_folder(form_request_object.files[key_name])
+        if key_name in request.files:
+            hed_file_path = save_file_to_upload_folder(request.files[key_name])
             hed_info[common_constants.HED_VERSION] = HedSchema.get_hed_xml_version(hed_file_path)
     except:
         hed_info[error_constants.ERROR_KEY] = traceback.format_exc()
     return hed_info
 
 
-def form_has_file(form_request_object, file_field, valid_extensions):
+def form_has_file(request, file_field, valid_extensions):
     """Checks to see if a file name with valid extension is present in the request object.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from the schema form.
     file_field: str
         Name of the form field containing the file name
@@ -118,19 +120,18 @@ def form_has_file(form_request_object, file_field, valid_extensions):
         True if a file is present in a request object.
 
     """
-    if file_field in form_request_object.files and \
-            file_extension_is_valid(form_request_object.files[file_field].filename, valid_extensions):
+    if file_field in request.files and file_extension_is_valid(request.files[file_field].filename, valid_extensions):
         return True
     else:
         return False
 
 
-def form_has_option(form_request_object, option_name, target_value):
+def form_has_option(request, option_name, target_value):
     """Checks if the given option has a specific value. This is used for radio buttons.
 
     Parameters
     ----------
-    form_request_object: Request
+    request: Request
         A Request object produced by the post of a form
     option_name: str
         String containing the name of the radio button group in the web form
@@ -143,17 +144,17 @@ def form_has_option(form_request_object, option_name, target_value):
         True if the target radio button has been set and false otherwise
     """
 
-    if option_name in form_request_object.values and form_request_object.values[option_name] == target_value:
+    if option_name in request.values and request.values[option_name] == target_value:
         return True
     return False
 
 
-def form_has_url(form_request_object, url_field, valid_extensions):
+def form_has_url(request, url_field, valid_extensions):
     """Checks to see if the url_field has a value with a valid extension.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from a form.
     url_field: str
         The name of the value field in the form containing the URL to be parsed.
@@ -166,11 +167,11 @@ def form_has_url(form_request_object, url_field, valid_extensions):
         True if a URL is present in request object.
 
     """
-    parsed_url = urlparse(form_request_object.form.get(url_field))
+    parsed_url = urlparse(request.form.get(url_field))
     return file_extension_is_valid(parsed_url.path, valid_extensions)
 
 
-def generate_download_file_response(download_file, display_name=None, header=None):
+def generate_download_file_response(download_file, display_name=None, header=None, category='success', msg=''):
     """Generates a download other response.
 
     Parameters
@@ -180,7 +181,11 @@ def generate_download_file_response(download_file, display_name=None, header=Non
     display_name: str
         Name to be assigned to the file in the response
     header: str
-        Optional header -- usually given for error message downloads
+        Optional header -- header for download file blob
+    category: str
+        Category of the message to be displayed ('Success', 'Error', 'Warning')
+    msg: str
+        Optional message to be displayed in the submit-flash-field
 
     Returns
     -------
@@ -190,6 +195,7 @@ def generate_download_file_response(download_file, display_name=None, header=Non
     """
     if not display_name:
         display_name = download_file
+    print("In generate:", display_name)
     try:
         if not download_file:
             return f"No download file given"
@@ -203,18 +209,54 @@ def generate():
                     yield header
                 for line in download:
                     yield line
-            delete_file_if_it_exist(download_file)
+            delete_file_if_it_exists(download_file)
         return Response(generate(), mimetype='text/plain charset=utf-8',
-                        headers={'Content-Disposition': f"attachment filename={display_name}"})
+                        headers={'Content-Disposition': f"attachment filename={display_name}",
+                                 'Category': category, 'Message': msg})
     except:
         return traceback.format_exc()
 
 
-def get_hed_path_from_pull_down(form_request_object):
+def generate_filename(basename, prefix=None, suffix=None, extension=None):
+    """Generates a filename for the attachment of the form prefix_basename_suffix + extension.
+
+    Parameters
+    ----------
+   basename: str
+        The name of the base, usually the name of the file that the issues were generated from
+    prefix: str
+        The prefix prepended to the front of the basename
+    suffix: str
+        The suffix appended to the end of the basename
+    Returns
+    -------
+    string
+        The name of the attachment other containing the issues.
+    """
+
+    pieces = []
+    if prefix:
+        pieces = pieces + [secure_filename(prefix)]
+    if basename:
+        pieces.append(os.path.splitext(secure_filename(basename))[0])
+    if suffix:
+        pieces = pieces + [secure_filename(suffix)]
+
+    if not pieces:
+        return ''
+    filename = pieces[0]
+    for name in pieces[1:]:
+        filename = filename + '_' + name
+    if extension:
+        filename = filename + '.' + secure_filename(extension)
+    return filename
+
+
+def get_hed_path_from_pull_down(request):
     """Gets the hed path from a section of form that uses a pull-down box and hed_cache
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from a form.
 
     Returns
@@ -222,28 +264,87 @@ def get_hed_path_from_pull_down(form_request_object):
     tuple: str, str
         The HED XML local path and the HED display file name
     """
-    if common_constants.HED_VERSION not in form_request_object.form:
+    if common_constants.HED_VERSION not in request.form:
         hed_file_path = ''
         hed_display_name = ''
-    elif form_request_object.form[common_constants.HED_VERSION] != common_constants.HED_OTHER_VERSION_OPTION:
-        hed_file_path = hed_cache.get_path_from_hed_version(form_request_object.form[common_constants.HED_VERSION])
+    elif request.form[common_constants.HED_VERSION] != common_constants.HED_OTHER_VERSION_OPTION:
+        hed_file_path = hed_cache.get_path_from_hed_version(request.form[common_constants.HED_VERSION])
         hed_display_name = os.path.basename(hed_file_path)
-    elif form_request_object.form[common_constants.HED_VERSION] == common_constants.HED_OTHER_VERSION_OPTION and \
-            common_constants.HED_XML_FILE in form_request_object.files:
-        hed_file_path = save_file_to_upload_folder(form_request_object.files[common_constants.HED_XML_FILE])
-        hed_display_name = form_request_object.files[common_constants.HED_XML_FILE].filename
+    elif request.form[common_constants.HED_VERSION] == common_constants.HED_OTHER_VERSION_OPTION and \
+            common_constants.HED_XML_FILE in request.files:
+        hed_file_path = save_file_to_upload_folder(request.files[common_constants.HED_XML_FILE])
+        hed_display_name = request.files[common_constants.HED_XML_FILE].filename
     else:
         hed_file_path = ''
         hed_display_name = ''
     return hed_file_path, hed_display_name
 
 
-def get_uploaded_file_path_from_form(form_request_object, file_key, valid_extensions=None):
+def get_printable_issue_string(issues, display_name=None, title_string=''):
+    """Returns a string suitable for text blob download
+
+     Parameters
+     ----------
+     issues: list
+         List of the issues to be printed.
+     title_string: str
+         key name in the files dictionary of the Request object
+     display_name: list of str
+         List of valid extensions
+
+     Returns
+     -------
+     tuple
+         A tuple containing the other paths. The two other paths are for the spreadsheet and a optional HED XML other.
+     """
+    if not issues:
+        return ''
+    if title_string:
+        title = title_string
+    else:
+        title = ''
+    if display_name:
+        title = display_name + ' ' + title_string + '\n'
+    if not title:
+        title = None
+    return ErrorHandler.get_printable_issue_string(issues, title, skip_filename=True)
+
+
+def get_optional_form_field(request, form_field_name, type=''):
+    """Gets the specified optional form field if present.
+
+    Parameters
+    ----------
+    request: Request object
+        A Request object containing user data from the validation form.
+    form_field_name: string
+        The name of the optional form field.
+    type: str
+        Name of expected type: 'boolean' or 'string'
+
+    Returns
+    -------
+    boolean or string
+        A boolean or string value based on the form field type.
+
+    """
+    form_field_value = ''
+    if type == common_constants.BOOLEAN:
+        form_field_value = False
+        if form_field_name in request.form:
+            form_field_value = True
+    elif type == common_constants.STRING:
+        if form_field_name in request.form:
+            form_field_value = request.form[form_field_name]
+    return form_field_value
+
+
+def get_uploaded_file_path_from_form(request, file_key, valid_extensions=None):
     """Gets the other paths of the uploaded files in the form.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from a form.
     file_key: str
         key name in the files dictionary of the Request object
@@ -257,9 +358,9 @@ def get_uploaded_file_path_from_form(form_request_object, file_key, valid_extens
     """
     uploaded_file_name = ''
     original_file_name = ''
-    if form_has_file(form_request_object, file_key, valid_extensions):
-        uploaded_file_name = save_file_to_upload_folder(form_request_object.files[file_key])
-        original_file_name = form_request_object.files[file_key].filename
+    if form_has_file(request, file_key, valid_extensions):
+        uploaded_file_name = save_file_to_upload_folder(request.files[file_key])
+        original_file_name = request.files[file_key].filename
     return uploaded_file_name, original_file_name
 
 
@@ -314,3 +415,52 @@ def save_file_to_upload_folder(file_object, delete_on_close=False):
     except:
         file_path = ''
     return file_path
+
+
+def save_issues_to_upload_folder(issues, filename):
+    """Saves the issues found the upload folder as filename.
+
+    Parameters
+    ----------
+    issues: string
+        A string containing the validation issues.
+    filename: str
+        File name of the issue folder
+
+    Returns
+    -------
+    string
+        The name of the validation output other.
+
+    """
+
+    issues_file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], filename)
+    with open(issues_file_path, 'w', encoding='utf-8') as issues_file:
+        for val_issue in issues:
+            issues_file.write(val_issue['message'] + "\n")
+    issues_file.close()
+    return issues_file_path
+
+
+def save_text_to_upload_folder(text, filename):
+    """Saves a string in the upload folder as filename.
+
+    Parameters
+    ----------
+    text: string
+        A printable string.
+    filename: str
+        File name of the issue folder
+
+    Returns
+    -------
+    string
+        The name of the validation output other.
+
+    """
+
+    file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], filename)
+    with open(file_path, 'w', encoding='utf-8') as text_file:
+        text_file.write(text)
+    text_file.close()
+    return file_path
diff --git a/hedweb/requirements.txt b/hedweb/requirements.txt
index 0e86736c7..ea73d03d7 100644
--- a/hedweb/requirements.txt
+++ b/hedweb/requirements.txt
@@ -16,6 +16,6 @@ xlrd==1.2.0
 defusedxml==0.6.0
 portalocker==2.0.0
 python-dateutil==2.8.1
-pytz==2020.4
+pytz==2021.1
 semantic_version==2.8.5
 six==1.15.0


Test patch:
diff --git a/hedtools/tests/schema/test_schema_validator.py b/hedtools/tests/schema/test_schema_validator.py
index 3c7deb769..20c0bbf0b 100644
--- a/hedtools/tests/schema/test_schema_validator.py
+++ b/hedtools/tests/schema/test_schema_validator.py
@@ -1,7 +1,7 @@
 import unittest
 import os
 from hed.schema import schema_validator
-from hed.util.error_reporter import format_schema_warning
+from hed.util import error_reporter
 from hed.util.error_types import SchemaWarnings
 
 class Test(unittest.TestCase):
@@ -10,16 +10,17 @@ class Test(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
+        cls.error_handler = error_reporter.ErrorHandler()
         cls.schema_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), cls.schema_file)
 
     def validate_term_base(self, input_text, expected_issues):
         for text, issues in zip(input_text, expected_issues):
-            test_issues = schema_validator.validate_schema_term(text)
+            test_issues = schema_validator.validate_schema_term(text, self.error_handler)
             self.assertCountEqual(issues, test_issues)
 
     def validate_desc_base(self, input_descriptions, expected_issues):
         for description, issues in zip(input_descriptions, expected_issues):
-            test_issues = schema_validator.validate_schema_description("dummy", description)
+            test_issues = schema_validator.validate_schema_description("dummy", description, self.error_handler)
             self.assertCountEqual(issues, test_issues)
 
     def test_validate_schema(self):
@@ -36,12 +37,12 @@ def test_validate_schema_term(self):
             "@invalidcharatstart",
         ]
         expected_issues = [
-            format_schema_warning(SchemaWarnings.INVALID_CAPITALIZATION, test_terms[0], error_index=0, problem_char="i"),
+            self.error_handler.format_schema_warning(SchemaWarnings.INVALID_CAPITALIZATION, test_terms[0], error_index=0, problem_char="i"),
             [],
             [],
-            format_schema_warning(SchemaWarnings.INVALID_CHARACTERS_IN_TAG, test_terms[3], error_index=11,
+            self.error_handler.format_schema_warning(SchemaWarnings.INVALID_CHARACTERS_IN_TAG, test_terms[3], error_index=11,
                                   problem_char="#"),
-            format_schema_warning(SchemaWarnings.INVALID_CAPITALIZATION, test_terms[4], error_index=0,
+            self.error_handler.format_schema_warning(SchemaWarnings.INVALID_CAPITALIZATION, test_terms[4], error_index=0,
                                   problem_char="@"),
         ]
         self.validate_term_base(test_terms, expected_issues)
@@ -57,13 +58,13 @@ def test_validate_schema_description(self):
             [],
             [],
             [],
-            format_schema_warning(SchemaWarnings.INVALID_CHARACTERS_IN_DESC, "dummy", test_descs[3], error_index=60,
+            self.error_handler.format_schema_warning(SchemaWarnings.INVALID_CHARACTERS_IN_DESC, "dummy", test_descs[3], error_index=60,
                                   problem_char="@")
-            + format_schema_warning(SchemaWarnings.INVALID_CHARACTERS_IN_DESC, "dummy", test_descs[3], error_index=61,
+            + self.error_handler.format_schema_warning(SchemaWarnings.INVALID_CHARACTERS_IN_DESC, "dummy", test_descs[3], error_index=61,
                                   problem_char="$")
-            + format_schema_warning(SchemaWarnings.INVALID_CHARACTERS_IN_DESC, "dummy", test_descs[3], error_index=62,
+            + self.error_handler.format_schema_warning(SchemaWarnings.INVALID_CHARACTERS_IN_DESC, "dummy", test_descs[3], error_index=62,
                                   problem_char="%")
-            + format_schema_warning(SchemaWarnings.INVALID_CHARACTERS_IN_DESC, "dummy", test_descs[3], error_index=63,
+            + self.error_handler.format_schema_warning(SchemaWarnings.INVALID_CHARACTERS_IN_DESC, "dummy", test_descs[3], error_index=63,
                                   problem_char="*")
             ,
         ]
diff --git a/hedtools/tests/tools/test_tag_format.py b/hedtools/tests/tools/test_tag_format.py
index 14e853fa3..157e7c64a 100644
--- a/hedtools/tests/tools/test_tag_format.py
+++ b/hedtools/tests/tools/test_tag_format.py
@@ -13,6 +13,7 @@ class TestTagFormat(unittest.TestCase):
     def setUpClass(cls):
         hed_xml = os.path.join(os.path.dirname(os.path.abspath(__file__)), cls.schema_file)
         cls.tag_compare = TagFormat(hed_xml)
+        cls.error_handler = error_reporter.ErrorHandler()
 
 
 class TestConvertTag(TestTagFormat):
@@ -93,7 +94,7 @@ def test_tag_spaces_start_end(self):
         }
         expected_errors = {
             'leadingSpace':
-                error_reporter.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND, test_strings['leadingSpace'],
+                self.error_handler.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND, test_strings['leadingSpace'],
                                                             0, 20)
             ,
             'trailingSpace': [],
@@ -135,19 +136,19 @@ def test_tag_invalid_extension(self):
         }
         expected_errors = {
             'validThenInvalid':
-                error_reporter.format_schema_error(SchemaErrors.INVALID_PARENT_NODE, test_strings['validThenInvalid'],
+                self.error_handler.format_schema_error(SchemaErrors.INVALID_PARENT_NODE, test_strings['validThenInvalid'],
                                                             55, 60, 'Event'),
             'singleLevel':
-                error_reporter.format_schema_error(SchemaErrors.INVALID_PARENT_NODE, test_strings['singleLevel'],
+                self.error_handler.format_schema_error(SchemaErrors.INVALID_PARENT_NODE, test_strings['singleLevel'],
                                                             19, 28, 'Item/Object/Geometric'),
             'singleLevelAlreadyLong':
-                error_reporter.format_schema_error(SchemaErrors.INVALID_PARENT_NODE,
+                self.error_handler.format_schema_error(SchemaErrors.INVALID_PARENT_NODE,
                                                             test_strings['singleLevelAlreadyLong'],
                                                             25, 34, 'Item/Object/Geometric'),
             'twoLevels':
-                error_reporter.format_schema_error(SchemaErrors.INVALID_PARENT_NODE, test_strings['twoLevels'],
+                self.error_handler.format_schema_error(SchemaErrors.INVALID_PARENT_NODE, test_strings['twoLevels'],
                                                             19, 28, 'Item/Object/Geometric'),
-            'partialDuplicate': error_reporter.format_schema_error(SchemaErrors.INVALID_PARENT_NODE,
+            'partialDuplicate': self.error_handler.format_schema_error(SchemaErrors.INVALID_PARENT_NODE,
                                                                              test_strings['partialDuplicate'], 10, 14, 'Item'),
         }
         self.validator(test_strings, expected_results, expected_errors)
@@ -164,12 +165,12 @@ def test_tag_invalid(self):
             'validChild': 'InvalidEvent/Event',
         }
         expected_errors = {
-            'single': error_reporter.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
+            'single': self.error_handler.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
                                                                    test_strings['single'], 0, 12),
-            'invalidChild': error_reporter.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
+            'invalidChild': self.error_handler.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
                                                                          test_strings['invalidChild'], 0, 12),
 
-            'validChild': error_reporter.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
+            'validChild': self.error_handler.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
                                                                        test_strings['validChild'], 0, 12),
         }
         self.validator(test_strings, expected_results, expected_errors)
@@ -299,16 +300,16 @@ def test_tag_takes_value_invalid(self):
         }
         expected_errors = {
             'singleLevel':
-                error_reporter.format_schema_error(SchemaErrors.INVALID_PARENT_NODE,
+                self.error_handler.format_schema_error(SchemaErrors.INVALID_PARENT_NODE,
                                                             test_strings['singleLevel'], 31, 36, 'Event')
             ,
             'multiLevel':
-                error_reporter.format_schema_error(SchemaErrors.INVALID_PARENT_NODE,
+                self.error_handler.format_schema_error(SchemaErrors.INVALID_PARENT_NODE,
                                                             test_strings['multiLevel'], 37, 50,
                                                  'Event/Sensory-event')
             ,
             'mixed':
-                error_reporter.format_schema_error(SchemaErrors.INVALID_PARENT_NODE,
+                self.error_handler.format_schema_error(SchemaErrors.INVALID_PARENT_NODE,
                                                             test_strings['mixed'], 31, 50,
                                                  'Item/Sound/Environmental-sound')
             ,
@@ -326,7 +327,7 @@ def test_tag_spaces_start_end(self):
         }
         expected_errors = {
             'leadingSpace':
-                error_reporter.format_schema_error(SchemaErrors.INVALID_PARENT_NODE, test_strings['leadingSpace'],
+                self.error_handler.format_schema_error(SchemaErrors.INVALID_PARENT_NODE, test_strings['leadingSpace'],
                                                             12, 31, 'Item/Sound/Environmental-sound')
             ,
             'trailingSpace': [],
@@ -367,18 +368,18 @@ def test_tag_invalid_extension(self):
             'duplicate': 'Item/Object/Geometric/Item/Object/Geometric',
         }
         expected_errors = {
-            'validThenInvalid': error_reporter.format_schema_error(SchemaErrors.INVALID_PARENT_NODE,
+            'validThenInvalid': self.error_handler.format_schema_error(SchemaErrors.INVALID_PARENT_NODE,
                                                                              test_strings['validThenInvalid'], 61, 66, 'Event'),
-            'singleLevel': error_reporter.format_schema_error(SchemaErrors.INVALID_PARENT_NODE,
+            'singleLevel': self.error_handler.format_schema_error(SchemaErrors.INVALID_PARENT_NODE,
                                                                         test_strings['singleLevel'], 25, 34,
                                                              'Item/Object/Geometric'),
             'singleLevelAlreadyShort':
-                error_reporter.format_schema_error(SchemaErrors.INVALID_PARENT_NODE,
+                self.error_handler.format_schema_error(SchemaErrors.INVALID_PARENT_NODE,
                                                             test_strings['singleLevelAlreadyShort'],
                                                             19, 28, 'Item/Object/Geometric'),
-            'twoLevels': error_reporter.format_schema_error(SchemaErrors.INVALID_PARENT_NODE,
+            'twoLevels': self.error_handler.format_schema_error(SchemaErrors.INVALID_PARENT_NODE,
                                                                       test_strings['twoLevels'], 35, 40, 'Event'),
-            'duplicate': error_reporter.format_schema_error(SchemaErrors.INVALID_PARENT_NODE,
+            'duplicate': self.error_handler.format_schema_error(SchemaErrors.INVALID_PARENT_NODE,
                                                                       test_strings['duplicate'], 34, 43, 'Item/Object/Geometric')
         }
         self.validator(test_strings, expected_results, expected_errors)
@@ -399,28 +400,28 @@ def test_tag_invalid(self):
             'invalidWithExtension': 'InvalidEvent/InvalidExtension',
         }
         expected_errors = {
-            'invalidParentWithExistingGrandchild': error_reporter.format_schema_error(
+            'invalidParentWithExistingGrandchild': self.error_handler.format_schema_error(
                 SchemaErrors.INVALID_PARENT_NODE,
                 test_strings[
                                                                                          'invalidParentWithExistingGrandchild'],
                 32, 41,
                                                                                      'Item/Object/Geometric'),
-            'invalidChildWithExistingGrandchild': error_reporter.format_schema_error(
+            'invalidChildWithExistingGrandchild': self.error_handler.format_schema_error(
                 SchemaErrors.INVALID_PARENT_NODE,
                 test_strings[
                                                                                         'invalidChildWithExistingGrandchild'],
                 19, 28,
                                                                                     'Item/Object/Geometric'),
-            'invalidParentWithExistingChild': error_reporter.format_schema_error(
+            'invalidParentWithExistingChild': self.error_handler.format_schema_error(
                 SchemaErrors.INVALID_PARENT_NODE,
                 test_strings[
                                                                                     'invalidParentWithExistingChild'],
                 13, 22,
                                                                                 'Item/Object/Geometric'),
-            'invalidSingle': error_reporter.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
+            'invalidSingle': self.error_handler.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
                                                                           test_strings['invalidSingle'], 0, 12),
             'invalidWithExtension':
-                error_reporter.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
+                self.error_handler.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
                                                             test_strings['invalidWithExtension'], 0, 12),
         }
         self.validator(test_strings, expected_results, expected_errors)
@@ -503,7 +504,7 @@ def test_empty_strings(self):
             'emptyString': '',
         }
         expected_errors = {
-            'emptyString': error_reporter.format_schema_error(SchemaErrors.EMPTY_TAG_FOUND, '')
+            'emptyString': self.error_handler.format_schema_error(SchemaErrors.EMPTY_TAG_FOUND, '')
         }
         self.validator(test_strings, expected_results, expected_errors)
 
@@ -564,17 +565,17 @@ def test_string_invalid(self):
             'doubleWithValid': double + ', Car/Minivan',
         }
         expected_errors = {
-            'single': error_reporter.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
+            'single': self.error_handler.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
                                                                    test_strings['single'], 0, 12),
-            'double': error_reporter.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
+            'double': self.error_handler.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
                                                                    test_strings['double'], 0, 12),
-            'both': error_reporter.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
+            'both': self.error_handler.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
                                                                  test_strings['both'], 0, 12)
-                     + error_reporter.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
+                     + self.error_handler.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
                                                                  test_strings['both'], 14, 26),
-            'singleWithTwoValid': error_reporter.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
+            'singleWithTwoValid': self.error_handler.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
                                                                                test_strings['singleWithTwoValid'], 11, 23),
-            'doubleWithValid': error_reporter.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
+            'doubleWithValid': self.error_handler.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
                                                                             test_strings['doubleWithValid'], 0, 12),
         }
         self.validator(test_strings, expected_results, expected_errors)
@@ -712,7 +713,7 @@ def test_empty_strings(self):
             'emptyString': '',
         }
         expected_errors = {
-            'emptyString': error_reporter.format_schema_error(SchemaErrors.EMPTY_TAG_FOUND, '')
+            'emptyString': self.error_handler.format_schema_error(SchemaErrors.EMPTY_TAG_FOUND, '')
         }
         self.validator(test_strings, expected_results, expected_errors)
 
@@ -773,17 +774,17 @@ def test_string_invalid(self):
             'doubleWithValid': double + ', Item/Object/Man-made/Vehicle/Car/Minivan',
         }
         expected_errors = {
-            'single': error_reporter.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
+            'single': self.error_handler.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
                                                                    test_strings['single'], 0, 12),
-            'double': error_reporter.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
+            'double': self.error_handler.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
                                                                    test_strings['double'], 0, 12),
-            'both': error_reporter.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
+            'both': self.error_handler.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
                                                                  test_strings['both'], 0, 12)
-                     + error_reporter.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
+                     + self.error_handler.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
                                                                  test_strings['both'], 14, 26),
-            'singleWithTwoValid': error_reporter.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
+            'singleWithTwoValid': self.error_handler.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
                                                                                test_strings['singleWithTwoValid'], 11, 23),
-            'doubleWithValid': error_reporter.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
+            'doubleWithValid': self.error_handler.format_schema_error(SchemaErrors.NO_VALID_TAG_FOUND,
                                                                             test_strings['doubleWithValid'], 0, 12),
         }
         self.validator(test_strings, expected_results, expected_errors)
diff --git a/hedtools/tests/util/test_col_def_group.py b/hedtools/tests/util/test_col_def_group.py
new file mode 100644
index 000000000..036194a0a
--- /dev/null
+++ b/hedtools/tests/util/test_col_def_group.py
@@ -0,0 +1,90 @@
+import unittest
+import os
+
+from hed.util.column_def_group import ColumnDefGroup
+from hed.util.column_definition import ColumnDef
+from hed.util.exceptions import HedFileError
+from hed.util.hed_schema import HedSchema
+
+class Test(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.base_data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/')
+        hed_xml_file = os.path.join(cls.base_data_dir, "HED8.0.0-alpha.1.xml")
+        cls.hed_schema = HedSchema(hed_xml_file)
+        cls.json_filename = os.path.join(cls.base_data_dir, "both_types_events.json")
+        cls.default_sidecar = ColumnDefGroup(cls.json_filename)
+
+    def test_invalid_filenames(self):
+        # Handle missing or invalid files.
+        invalid_json = "invalidxmlfile.json"
+        json_dict = None
+        try:
+            json_dict = ColumnDefGroup(invalid_json)
+        except HedFileError as e:
+            pass
+
+        self.assertFalse(json_dict)
+
+        json_dict = None
+        try:
+            json_dict = ColumnDefGroup(None)
+        except HedFileError as e:
+            pass
+        self.assertTrue(len(json_dict._column_settings) == 0)
+
+        json_dict = None
+        try:
+            json_dict = ColumnDefGroup("")
+        except HedFileError as e:
+            pass
+        self.assertTrue(len(json_dict._column_settings) == 0)
+
+    def test_display_filename(self):
+        invalid_json = "invalidxmlfile.json"
+        display_filename = "PrettyDisplayName.json"
+        try:
+            json_dict = ColumnDefGroup(invalid_json)
+            self.assertTrue(False)
+        except HedFileError as e:
+            self.assertTrue(display_filename in e.format_error_message(return_string_only=True,
+                                                                       display_filename=display_filename))
+
+    def test_add_json_string(self):
+        with open(self.json_filename, "r") as fp:
+            input_string = fp.read()
+            json_file = ColumnDefGroup(json_string=input_string)
+            self.assertTrue(json_file)
+
+
+    def test__iter__(self):
+        columns_target = 3
+        columns_count = 0
+        for column_def in self.default_sidecar:
+            self.assertIsInstance(column_def, ColumnDef)
+            columns_count += 1
+
+        self.assertEqual(columns_target, columns_count)
+
+    # def save_as_json(self, save_filename):
+    #
+    # def add_json_file_defs(self, json_filename):
+    #
+    # @staticmethod
+    # def load_multiple_json_files(json_file_input_list):
+    #
+    # def hed_string_iter(self, include_position=False):
+    #
+    #
+    # def set_hed_string(self, new_hed_string, position):
+    #
+    #
+    # def _add_single_col_type(self, column_name, dict_for_entry, column_type=None):
+    #
+    # def get_validation_issues(self):
+    #
+    # def validate_entries(self, hed_schema=None, display_filename=None):
+    #
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/hedtools/tests/util/test_def_dict.py b/hedtools/tests/util/test_def_dict.py
new file mode 100644
index 000000000..aa377581c
--- /dev/null
+++ b/hedtools/tests/util/test_def_dict.py
@@ -0,0 +1,46 @@
+import unittest
+import os
+
+from hed.util.def_mapper import DefinitionMapper
+from hed.util.hed_schema import HedSchema
+from hed.util.def_dict import DefDict
+
+class Test(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.base_data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/')
+        hed_xml_file = os.path.join(cls.base_data_dir, "HED8.0.0-alpha.1.xml")
+        cls.hed_schema = HedSchema(hed_xml_file)
+        cls.base_def_mapper = DefinitionMapper(hed_schema=cls.hed_schema)
+        cls.def_contents_string = "(Item/TestDef1, Item/TestDef2)"
+        cls.basic_def_string = f"(Definition/TestDef, Organizational/TestOrg, {cls.def_contents_string})"
+        cls.label_def_string = f"Label-def/TestDef"
+        cls.expanded_def_string = f"(Label-exp/TestDef, Organizational/TestOrg, {cls.def_contents_string})"
+        cls.basic_hed_string = "Item/BasicTestTag1, Item/BasicTestTag2"
+        cls.basic_hed_string_with_def = f"Item/BasicTestTag1, Item/BasicTestTag2, {cls.label_def_string}"
+
+    def test_check_for_definitions(self):
+        def_dict = DefDict(hed_schema=self.hed_schema)
+        original_def_count = len(def_dict._defs)
+        def_dict.check_for_definitions(self.basic_def_string)
+        new_def_count = len(def_dict._defs)
+        self.assertGreater(new_def_count, original_def_count)
+
+    def test__check_tag_starts_with(self):
+        possible_tag_list = ["definition", "informational/definition", "attribute/informational/definition"]
+
+        test_tags = ["Definition/TempTestDef", "Informational/Definition/TempTestDef",
+                     "Attribute/Informational/Definition/TempTestDef"]
+
+        for tag in test_tags:
+            result = DefinitionMapper._check_tag_starts_with(tag, possible_tag_list)
+            self.assertTrue(result)
+
+
+if __name__ == '__main__':
+    unittest.main()
+
+
+
+
+
diff --git a/hedtools/tests/util/test_def_mapper.py b/hedtools/tests/util/test_def_mapper.py
index 7676b53fb..4dba8de0a 100644
--- a/hedtools/tests/util/test_def_mapper.py
+++ b/hedtools/tests/util/test_def_mapper.py
@@ -3,6 +3,7 @@
 
 from hed.util.def_mapper import DefinitionMapper
 from hed.util.hed_schema import HedSchema
+from hed.util.def_dict import DefDict
 
 class Test(unittest.TestCase):
     @classmethod
@@ -10,20 +11,18 @@ def setUpClass(cls):
         cls.base_data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/')
         hed_xml_file = os.path.join(cls.base_data_dir, "HED8.0.0-alpha.1.xml")
         cls.hed_schema = HedSchema(hed_xml_file)
-        cls.base_def_mapper = DefinitionMapper(hed_schema=cls.hed_schema)
-        cls.basic_def_string = "(Definition/TestDef, Organizational/TestOrg, (Item/TestDef1, Item/TestDef2))"
+        cls.base_dict = DefDict(hed_schema=cls.hed_schema)
+        cls.def_contents_string = "(Item/TestDef1, Item/TestDef2)"
+        cls.basic_def_string = f"(Definition/TestDef, Organizational/TestOrg, {cls.def_contents_string})"
+        cls.label_def_string = f"Label-def/TestDef"
+        cls.expanded_def_string = f"(Label-exp/TestDef, Organizational/TestOrg, {cls.def_contents_string})"
         cls.basic_hed_string = "Item/BasicTestTag1, Item/BasicTestTag2"
-
-    def test_add_definitions(self):
-        def_mapper = DefinitionMapper(hed_schema=self.hed_schema)
-        original_def_count = len(def_mapper._defs)
-        def_mapper.add_definitions(self.basic_def_string)
-        new_def_count = len(def_mapper._defs)
-        self.assertGreater(new_def_count, original_def_count)
+        cls.basic_hed_string_with_def = f"Item/BasicTestTag1, Item/BasicTestTag2, {cls.label_def_string}"
 
     def test_replace_and_remove_tags(self):
-        def_mapper = DefinitionMapper(hed_schema=self.hed_schema)
-        def_mapper.add_definitions(self.basic_def_string)
+        def_dict = DefDict(hed_schema=self.hed_schema)
+        def_dict.check_for_definitions(self.basic_def_string)
+        def_mapper = DefinitionMapper(def_dict, hed_schema=self.hed_schema)
 
         result_string = def_mapper.replace_and_remove_tags(self.basic_def_string)
         self.assertEqual(result_string, "")
@@ -34,6 +33,9 @@ def test_replace_and_remove_tags(self):
         result_string = def_mapper.replace_and_remove_tags(self.basic_def_string + ", " + self.basic_hed_string)
         self.assertEqual(result_string, self.basic_hed_string)
 
+        result_string = def_mapper.replace_and_remove_tags(self.basic_hed_string_with_def)
+        self.assertEqual(result_string, self.basic_hed_string + ", " + self.expanded_def_string)
+
     def test__check_tag_starts_with(self):
         possible_tag_list = ["definition", "informational/definition", "attribute/informational/definition"]
 
@@ -44,13 +46,6 @@ def test__check_tag_starts_with(self):
             result = DefinitionMapper._check_tag_starts_with(tag, possible_tag_list)
             self.assertTrue(result)
 
-    def test__check_for_definitions(self):
-        def_mapper = DefinitionMapper(hed_schema=self.hed_schema)
-        original_def_count = len(def_mapper._defs)
-        def_mapper._check_for_definitions(self.basic_def_string)
-        new_def_count = len(def_mapper._defs)
-        self.assertGreater(new_def_count, original_def_count)
-
     def test__find_tag_group_extent(self):
         input_strings = [
             (1, self.basic_def_string),
diff --git a/hedtools/tests/util/test_error_reporter.py b/hedtools/tests/util/test_error_reporter.py
index 4f79aa1e7..9ca251a62 100644
--- a/hedtools/tests/util/test_error_reporter.py
+++ b/hedtools/tests/util/test_error_reporter.py
@@ -1,52 +1,75 @@
 import unittest
 from hed.util import error_reporter
 from hed.util import error_types
+from hed.util.error_types import ErrorContext, ErrorSeverity
+
 
 class Test(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
+        cls.error_handler = error_reporter.ErrorHandler()
         pass
 
     def test_push_error_context(self):
-        error_list = error_reporter.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        error_list = self.error_handler.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
         self.assertTrue(len(error_list) == 1)
-        error_reporter.push_error_context(error_types.ErrorContext.FILE_NAME, "DummyFileName.txt")
-        error_list = error_reporter.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
-        self.assertTrue(len(error_list) == 2)
-        error_reporter.push_error_context(error_types.ErrorContext.SIDECAR_COLUMN_NAME, "DummyColumnName")
-        error_list = error_reporter.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
-        self.assertTrue(len(error_list) == 2)
-        error_reporter.reset_error_context()
-        error_reporter.push_error_context(error_types.ErrorContext.FILE_NAME, "DummyFileName.txt")
-        error_reporter.push_error_context(error_types.ErrorContext.SIDECAR_COLUMN_NAME, "DummyColumnName")
-        error_reporter.push_error_context(error_types.ErrorContext.COLUMN, 1, column_context=3)
-        error_list = error_reporter.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
-        self.assertTrue(len(error_list) == 4)
-        error_reporter.reset_error_context()
+        display_filename = "DummyFileName.txt"
+        self.error_handler.push_error_context(ErrorContext.FILE_NAME, display_filename)
+        error_list = self.error_handler.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        self.assertTrue(display_filename in error_list[0][ErrorContext.FILE_NAME])
+        column_name = "DummyColumnName"
+        self.error_handler.push_error_context(ErrorContext.SIDECAR_COLUMN_NAME, column_name)
+        error_list = self.error_handler.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        self.assertTrue(column_name in error_list[0][ErrorContext.SIDECAR_COLUMN_NAME])
+        self.error_handler.reset_error_context()
+        self.error_handler.push_error_context(ErrorContext.FILE_NAME, display_filename)
+        self.error_handler.push_error_context(ErrorContext.SIDECAR_COLUMN_NAME, column_name)
+        self.error_handler.push_error_context(ErrorContext.COLUMN, 1)
+        error_list = self.error_handler.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        self.assertTrue(display_filename in error_list[0][ErrorContext.FILE_NAME])
+        self.assertTrue(column_name in error_list[0][ErrorContext.SIDECAR_COLUMN_NAME])
+        self.assertTrue(1 in error_list[0][ErrorContext.COLUMN])
+        self.assertTrue(len(error_list) == 1)
+        self.error_handler.reset_error_context()
 
     def test_pop_error_context(self):
-        error_list = error_reporter.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        error_list = self.error_handler.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
         self.assertTrue(len(error_list) == 1)
-        error_reporter.push_error_context(error_types.ErrorContext.FILE_NAME, "DummyFileName.txt")
-        error_list = error_reporter.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
-        self.assertTrue(len(error_list) == 2)
-        error_reporter.pop_error_context()
-        error_list = error_reporter.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        display_filename = "DummyFileName.txt"
+        self.error_handler.push_error_context(ErrorContext.FILE_NAME, display_filename)
+        error_list = self.error_handler.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        self.assertTrue(len(error_list) == 1)
+        self.assertTrue(display_filename in error_list[0][ErrorContext.FILE_NAME])
+        self.error_handler.pop_error_context()
+        error_list = self.error_handler.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
         self.assertTrue(len(error_list) == 1)
-        error_reporter.push_error_context(error_types.ErrorContext.SIDECAR_COLUMN_NAME, "DummyColumnName")
-        error_list = error_reporter.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        column_name = "DummyColumnName"
+        self.error_handler.push_error_context(ErrorContext.SIDECAR_COLUMN_NAME, column_name)
+        error_list = self.error_handler.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        self.assertTrue(len(error_list) == 1)
+        self.error_handler.push_error_context(ErrorContext.FILE_NAME, display_filename)
+        self.error_handler.push_error_context(ErrorContext.SIDECAR_COLUMN_NAME, column_name)
+        self.error_handler.push_error_context(ErrorContext.COLUMN, 1)
+        error_list = self.error_handler.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        self.assertTrue(len(error_list) == 1)
+        self.assertTrue(display_filename in error_list[0][ErrorContext.FILE_NAME])
+        self.assertTrue(column_name in error_list[0][ErrorContext.SIDECAR_COLUMN_NAME])
+        self.assertTrue(1 in error_list[0][ErrorContext.COLUMN])
+        self.error_handler.pop_error_context()
+        self.error_handler.pop_error_context()
+        self.error_handler.pop_error_context()
+        error_list = self.error_handler.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        self.assertTrue(len(error_list) == 1)
+        self.assertTrue(ErrorContext.COLUMN not in error_list[0])
+        self.error_handler.pop_error_context()
+        error_list = self.error_handler.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        self.assertTrue(len(error_list) == 1)
+        self.error_handler.reset_error_context()
+
+    def test_filter_issues_by_severity(self):
+        error_list = self.error_handler.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
+        error_list += self.error_handler.format_schema_warning(error_types.SchemaWarnings.INVALID_CAPITALIZATION,
+                                                               "invalid_caps")
         self.assertTrue(len(error_list) == 2)
-        error_reporter.push_error_context(error_types.ErrorContext.FILE_NAME, "DummyFileName.txt")
-        error_reporter.push_error_context(error_types.ErrorContext.SIDECAR_COLUMN_NAME, "DummyColumnName")
-        error_reporter.push_error_context(error_types.ErrorContext.COLUMN, 1, column_context=3)
-        error_list = error_reporter.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
-        self.assertTrue(len(error_list) == 4)
-        error_reporter.pop_error_context()
-        error_reporter.pop_error_context()
-        error_reporter.pop_error_context()
-        error_list = error_reporter.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
-        self.assertTrue(len(error_list) == 1)
-        error_reporter.pop_error_context()
-        error_list = error_reporter.format_schema_error(error_types.SchemaErrors.EMPTY_TAG_FOUND, "")
-        self.assertTrue(len(error_list) == 1)
-        error_reporter.reset_error_context()
+        filtered_list = self.error_handler.filter_issues_by_severity(issues_list=error_list, severity=ErrorSeverity.ERROR)
+        self.assertTrue(len(filtered_list) == 1)
diff --git a/hedtools/tests/util/test_file_util.py b/hedtools/tests/util/test_file_util.py
index 4946ec9d6..97d198eff 100644
--- a/hedtools/tests/util/test_file_util.py
+++ b/hedtools/tests/util/test_file_util.py
@@ -11,13 +11,24 @@ def setUpClass(cls):
     def test_url_to_file(self):
         downloaded_file = file_util.url_to_file(self.default_test_url)
         self.assertTrue(downloaded_file)
-        file_util.delete_file_if_it_exist(downloaded_file)
+        file_util.delete_file_if_it_exists(downloaded_file)
 
     def test_delete_file_if_it_exist(self):
         some_file = '3k32j23kj.txt'
-        deleted = file_util.delete_file_if_it_exist(some_file)
+        deleted = file_util.delete_file_if_it_exists(some_file)
         self.assertFalse(deleted)
 
+        deleted = file_util.delete_file_if_it_exists(None)
+        self.assertFalse(deleted)
+
+        deleted = file_util.delete_file_if_it_exists("")
+        self.assertFalse(deleted)
+
+        some_file = "folder_that_does_not_exist/3k32j23kj.txt"
+        deleted = file_util.delete_file_if_it_exists(some_file)
+        self.assertFalse(deleted)
+
+
     def test_get_file_extension(self):
         expected_extension = '.xml'
         file_extension = file_util.get_file_extension(self.hed_xml_file)
diff --git a/hedtools/tests/util/test_hed_schema.py b/hedtools/tests/util/test_hed_schema.py
index f35d0a677..b49b890ac 100644
--- a/hedtools/tests/util/test_hed_schema.py
+++ b/hedtools/tests/util/test_hed_schema.py
@@ -2,6 +2,7 @@
 import os
 
 from hed.util.hed_schema import HedSchema
+from hed.util.exceptions import HedFileError
 
 
 class TestHedSchema(unittest.TestCase):
@@ -13,6 +14,42 @@ def setUpClass(cls):
         cls.hed_schema = HedSchema(hed_xml)
         cls.hed_schema_dictionaries = cls.hed_schema.dictionaries
 
+    def test_invalid_schema(self):
+        # Handle missing or invalid files.
+        invalid_xml_file = "invalidxmlfile.xml"
+        hed_schema = None
+        try:
+            hed_schema = HedSchema(invalid_xml_file)
+        except HedFileError as e:
+            pass
+
+        self.assertFalse(hed_schema)
+
+        hed_schema = None
+        try:
+            hed_schema = HedSchema(None)
+        except HedFileError as e:
+            pass
+        self.assertFalse(hed_schema)
+
+        hed_schema = None
+        try:
+            hed_schema = HedSchema("")
+        except HedFileError as e:
+            pass
+        self.assertFalse(hed_schema)
+
+    def test_display_filename(self):
+        invalid_xml_file = "invalidxmlfile.xml"
+        display_filename = "PrettyDisplayName.xml"
+        try:
+            hed_schema = HedSchema(invalid_xml_file)
+            # We should have an error before we reach here.
+            self.assertTrue(False)
+        except HedFileError as e:
+            self.assertTrue(display_filename in e.format_error_message(return_string_only=True,
+                                                                       display_filename=display_filename))
+
     def test_attribute_keys(self):
         tag_dictionary_keys = ['default', 'extensionAllowed', 'isNumeric', 'position', 'predicateType', 'recommended',
                                'required', 'requireChild', 'tags', 'takesValue', 'unique', 'unitClass']
diff --git a/hedtools/tests/validator/test_hed_validator.py b/hedtools/tests/validator/test_hed_validator.py
index fedd4b7f6..b28f5498f 100644
--- a/hedtools/tests/validator/test_hed_validator.py
+++ b/hedtools/tests/validator/test_hed_validator.py
@@ -1,14 +1,18 @@
 import random
 import unittest
+import os
 
 from hed.util.hed_string_delimiter import HedStringDelimiter
 from hed.validator.hed_validator import HedValidator
+from hed.util.hed_file_input import HedFileInput
+from hed.util.error_types import ErrorContext
 
 
 class Test(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
-        cls.generic_hed_input_reader = HedValidator('Attribute/Temporal/Onset', xml_version_number='7.1.1')
+        cls.base_hed_input = 'Attribute/Temporal/Onset'
+        cls.generic_hed_input_reader = HedValidator(xml_version_number='7.1.1')
         cls.text_file_with_extension = 'file_with_extension.txt'
         cls.integer_key_dictionary = {1: 'one', 2: 'two', 3: 'three'}
         cls.float_value = 1.1
@@ -34,11 +38,20 @@ def setUpClass(cls):
         cls.row_hed_tag_columns = [1, 2]
         cls.original_and_formatted_tag = [('Event/Label/Test', 'event/label/test'),
                                           ('Event/Description/Test', 'event/description/test')]
+        cls.hed_base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data')
+        cls.hed_filepath_with_errors = os.path.join(cls.hed_base_dir, "ExcelMultipleSheets.xlsx")
+        cls.hed_file_with_errors = HedFileInput(cls.hed_filepath_with_errors)
 
-    def test__validate_hed_input(self):
-        validation_issues = self.generic_hed_input_reader._validate_hed_input()
+    def test__validate_input(self):
+        validation_issues = self.generic_hed_input_reader.validate_input(self.base_hed_input)
         self.assertIsInstance(validation_issues, list)
 
+        display_filename = "DummyDisplayFilename.txt"
+        validation_issues = self.generic_hed_input_reader.validate_input(self.hed_file_with_errors,
+                                                                         display_filename=display_filename)
+        self.assertIsInstance(validation_issues, list)
+        self.assertTrue(display_filename in validation_issues[0][ErrorContext.FILE_NAME])
+
     def test__validate_individual_tags_in_hed_string(self):
         hed_string_delimiter = HedStringDelimiter(self.hed_string_with_invalid_tags)
         validation_issues = self.generic_hed_input_reader._validate_individual_tags_in_hed_string(hed_string_delimiter)
@@ -100,11 +113,6 @@ def test_validate_column_hed_string(self):
         self.assertIsInstance(validation_issues, list)
         self.assertTrue(validation_issues)
 
-    def test_get_validation_issues(self):
-        validation_issues = self.generic_hed_input_reader.get_validation_issues()
-        self.assertIsInstance(validation_issues, list)
-
-
     def test_get_previous_original_and_formatted_tag(self):
         loop_index = 1
         previous_original_tag, previous_formatted_tag = HedValidator.get_previous_original_and_formatted_tag(
diff --git a/hedtools/tests/validator/test_tag_validator.py b/hedtools/tests/validator/test_tag_validator.py
index d1bfa6618..22a8f2ab1 100644
--- a/hedtools/tests/validator/test_tag_validator.py
+++ b/hedtools/tests/validator/test_tag_validator.py
@@ -3,7 +3,7 @@
 
 from hed.util.hed_string_delimiter import HedStringDelimiter
 from hed.validator.hed_validator import HedValidator
-from hed.util.error_reporter import format_val_error, format_val_warning
+from hed.util import error_reporter
 from hed.validator.tag_validator import TagValidator
 from hed.util.hed_schema import HedSchema
 
@@ -15,6 +15,7 @@ class TestHed(unittest.TestCase):
     def setUpClass(cls):
         hed_xml = os.path.join(os.path.dirname(os.path.abspath(__file__)), cls.schema_file)
         cls.hed_schema = HedSchema(hed_xml)
+        cls.error_handler = error_reporter.ErrorHandler()
         cls.syntactic_tag_validator = TagValidator(check_for_warnings=False,
                                                    run_semantic_validation=False)
         cls.syntactic_warning_tag_validator = TagValidator(check_for_warnings=True,
@@ -23,13 +24,13 @@ def setUpClass(cls):
                                                   run_semantic_validation=True)
         cls.semantic_warning_tag_validator = TagValidator(cls.hed_schema, check_for_warnings=True,
                                                           run_semantic_validation=True)
-        cls.syntactic_hed_input_reader = HedValidator("Event/Category")
+        cls.syntactic_hed_input_reader = HedValidator()
         cls.syntactic_hed_input_reader._tag_validator = cls.syntactic_tag_validator
-        cls.syntactic_warning_hed_input_reader = HedValidator("Event/Category")
+        cls.syntactic_warning_hed_input_reader = HedValidator()
         cls.syntactic_warning_hed_input_reader._tag_validator = cls.syntactic_warning_tag_validator
-        cls.semantic_hed_input_reader = HedValidator("Event/Category")
+        cls.semantic_hed_input_reader = HedValidator()
         cls.semantic_hed_input_reader._tag_validator = cls.semantic_tag_validator
-        cls.semantic_warning_hed_input_reader = HedValidator("Event/Category")
+        cls.semantic_warning_hed_input_reader = HedValidator()
         cls.semantic_warning_hed_input_reader._tag_validator = cls.semantic_warning_tag_validator
 
     def validator_base(self, test_strings, expected_results, expected_issues, test_function):
@@ -72,9 +73,9 @@ def test_mismatched_parentheses(self):
             'valid': True
         }
         expected_issues = {
-            'extraOpening': format_val_error('parentheses',
+            'extraOpening': self.error_handler.format_val_error('parentheses',
                                              opening_parentheses_count=2, closing_parentheses_count=1),
-            'extraClosing': format_val_error('parentheses',
+            'extraClosing': self.error_handler.format_val_error('parentheses',
                                              opening_parentheses_count=1, closing_parentheses_count=2),
             'valid': []
         }
@@ -154,43 +155,43 @@ def test_malformed_delimiters(self):
             'invalidNestedParentheses': False,
         }
         expected_issues = {
-            'missingOpeningComma': format_val_error('invalidTag', tag='/Action/Reach/To touch('),
-            'missingClosingComma': format_val_error('commaMissing', tag='/Participant/Effect/Body part/Arm)'),
-            'extraOpeningComma': format_val_error('extraDelimiter', character=',', index=0,
+            'missingOpeningComma': self.error_handler.format_val_error('invalidTag', tag='/Action/Reach/To touch('),
+            'missingClosingComma': self.error_handler.format_val_error('commaMissing', tag='/Participant/Effect/Body part/Arm)'),
+            'extraOpeningComma': self.error_handler.format_val_error('extraDelimiter', character=',', index=0,
                                                   hed_string=test_strings['extraOpeningComma']),
-            'extraClosingComma': format_val_error('extraDelimiter', character=',',
+            'extraClosingComma': self.error_handler.format_val_error('extraDelimiter', character=',',
                                                   index=len(test_strings['extraClosingComma']) - 1,
                                                   hed_string=test_strings['extraClosingComma']),
-            'extraOpeningTilde': format_val_error('extraDelimiter', character='~', index=0,
+            'extraOpeningTilde': self.error_handler.format_val_error('extraDelimiter', character='~', index=0,
                                                   hed_string=test_strings['extraOpeningTilde']),
-            'extraClosingTilde': format_val_error('extraDelimiter', character='~',
+            'extraClosingTilde': self.error_handler.format_val_error('extraDelimiter', character='~',
                                                   index=len(test_strings['extraClosingTilde']) - 1,
                                                   hed_string=test_strings['extraClosingTilde']),
-            'multipleExtraOpeningDelimiters': format_val_error('extraDelimiter', character=',', index=0,
+            'multipleExtraOpeningDelimiters': self.error_handler.format_val_error('extraDelimiter', character=',', index=0,
                                                                hed_string=test_strings[
                                                                     'multipleExtraOpeningDelimiters'])
-                                              + format_val_error('extraDelimiter', character='~', index=1,
+                                              + self.error_handler.format_val_error('extraDelimiter', character='~', index=1,
                                                                  hed_string=test_strings[
                                                                       'multipleExtraOpeningDelimiters'])
-                                              + format_val_error('extraDelimiter', character=',', index=2,
+                                              + self.error_handler.format_val_error('extraDelimiter', character=',', index=2,
                                                                  hed_string=test_strings[
                                                                       'multipleExtraOpeningDelimiters']),
-            'multipleExtraClosingDelimiters': format_val_error('extraDelimiter', character=',', index=len(
+            'multipleExtraClosingDelimiters': self.error_handler.format_val_error('extraDelimiter', character=',', index=len(
                 test_strings['multipleExtraClosingDelimiters']) - 1, hed_string=test_strings[
-                'multipleExtraClosingDelimiters']) + format_val_error('extraDelimiter', character='~', index=len(
+                'multipleExtraClosingDelimiters']) + self.error_handler.format_val_error('extraDelimiter', character='~', index=len(
                 test_strings['multipleExtraClosingDelimiters']) - 2, hed_string=test_strings[
-                'multipleExtraClosingDelimiters']) + format_val_error('extraDelimiter', character='~', index=len(
+                'multipleExtraClosingDelimiters']) + self.error_handler.format_val_error('extraDelimiter', character='~', index=len(
                 test_strings['multipleExtraClosingDelimiters']) - 3, hed_string=test_strings[
-                'multipleExtraClosingDelimiters']) + format_val_error('extraDelimiter', character=',', index=len(
+                'multipleExtraClosingDelimiters']) + self.error_handler.format_val_error('extraDelimiter', character=',', index=len(
                 test_strings['multipleExtraClosingDelimiters']) - 4, hed_string=test_strings[
                 'multipleExtraClosingDelimiters']),
-            'multipleExtraMiddleDelimiters': format_val_error('extraDelimiter', character=',', index=23,
+            'multipleExtraMiddleDelimiters': self.error_handler.format_val_error('extraDelimiter', character=',', index=23,
                                                               hed_string=test_strings[
                                                                    'multipleExtraMiddleDelimiters'])
-                                             + format_val_error('extraDelimiter', character='~', index=125,
+                                             + self.error_handler.format_val_error('extraDelimiter', character='~', index=125,
                                                                 hed_string=test_strings[
                                                                      'multipleExtraMiddleDelimiters'])
-                                             + format_val_error('extraDelimiter',
+                                             + self.error_handler.format_val_error('extraDelimiter',
                                                                 character=',', index=126,
                                                                 hed_string=test_strings[
                                                                      'multipleExtraMiddleDelimiters']),
@@ -199,7 +200,7 @@ def test_malformed_delimiters(self):
             'validNestedParentheses2': [],
             'validNestedParentheses3': [],
             'validNestedParentheses4': [],
-            'invalidNestedParentheses': format_val_error('commaMissing', tag="Thing)) ")
+            'invalidNestedParentheses': self.error_handler.format_val_error('commaMissing', tag="Thing)) ")
         }
         self.validator(test_strings, expected_results, expected_issues)
 
@@ -221,13 +222,13 @@ def test_invalid_characters(self):
             'closingBracket': False
         }
         expected_issues = {
-            'openingBrace': format_val_error('invalidCharacter', character='{', index=47,
+            'openingBrace': self.error_handler.format_val_error('invalidCharacter', character='{', index=47,
                                              hed_string=test_strings['openingBrace']),
-            'closingBrace': format_val_error('invalidCharacter', character='}', index=47,
+            'closingBrace': self.error_handler.format_val_error('invalidCharacter', character='}', index=47,
                                              hed_string=test_strings['closingBrace']),
-            'openingBracket': format_val_error('invalidCharacter', character='[', index=47,
+            'openingBracket': self.error_handler.format_val_error('invalidCharacter', character='[', index=47,
                                                hed_string=test_strings['openingBracket']),
-            'closingBracket': format_val_error('invalidCharacter', character=']', index=47,
+            'closingBracket': self.error_handler.format_val_error('invalidCharacter', character=']', index=47,
                                                hed_string=test_strings['closingBracket'])
         }
         self.validator(test_strings, expected_results, expected_issues)
@@ -275,9 +276,9 @@ def test_exist_in_schema(self):
             'takesValue': [],
             'full': [],
             'extensionsAllowed': [],
-            'leafExtension': format_val_error('invalidTag', tag=test_strings['leafExtension']),
-            'nonExtensionsAllowed': format_val_error('invalidTag', tag=test_strings['nonExtensionsAllowed']),
-            'illegalComma': format_val_error('extraCommaOrInvalid', previous_tag='Event/Label/This is a label',
+            'leafExtension': self.error_handler.format_val_error('invalidTag', tag=test_strings['leafExtension']),
+            'nonExtensionsAllowed': self.error_handler.format_val_error('invalidTag', tag=test_strings['nonExtensionsAllowed']),
+            'illegalComma': self.error_handler.format_val_error('extraCommaOrInvalid', previous_tag='Event/Label/This is a label',
                                              tag='This/Is/A/Tag')
         }
         self.validator_semantic(test_strings, expected_results, expected_issues, False)
@@ -302,7 +303,7 @@ def test_proper_capitalization(self):
             'camelCase': [],
             'takesValue': [],
             'numeric': [],
-            'lowercase': format_val_warning('capitalization', tag=test_strings['lowercase'])
+            'lowercase': self.error_handler.format_val_warning('capitalization', tag=test_strings['lowercase'])
         }
         self.validator_syntactic(test_strings, expected_results, expected_issues, True)
 
@@ -317,7 +318,7 @@ def test_child_required(self):
         }
         expected_issues = {
             'hasChild': [],
-            'missingChild': format_val_error('childRequired', tag=test_strings['missingChild'])
+            'missingChild': self.error_handler.format_val_error('childRequired', tag=test_strings['missingChild'])
         }
         self.validator_semantic(test_strings, expected_results, expected_issues, True)
 
@@ -343,13 +344,13 @@ def test_required_units(self):
         legal_clock_time_units = ['hour:min', 'hour:min:sec']
         expected_issues = {
             'hasRequiredUnit': [],
-            'missingRequiredUnit': format_val_warning('unitClassDefaultUsed', tag=test_strings['missingRequiredUnit'],
+            'missingRequiredUnit': self.error_handler.format_val_warning('unitClassDefaultUsed', tag=test_strings['missingRequiredUnit'],
                                                       default_unit='s'),
             'notRequiredNoNumber': [],
             'notRequiredNumber': [],
             'notRequiredScientific': [],
             'timeValue': [],
-            'invalidTimeValue':format_val_error('unitClassInvalidUnit', tag=test_strings['invalidTimeValue'],
+            'invalidTimeValue':self.error_handler.format_val_error('unitClassInvalidUnit', tag=test_strings['invalidTimeValue'],
                                                 unit_class_units=",".join(sorted(legal_clock_time_units))),
         }
         self.validator_semantic(test_strings, expected_results, expected_issues, True)
@@ -398,21 +399,21 @@ def test_correct_units(self):
             'correctNoPluralUnit': [],
             'correctNonSymbolCapitalizedUnit': [],
             'correctSymbolCapitalizedUnit': [],
-            'incorrectUnit': format_val_error('unitClassInvalidUnit', tag=test_strings['incorrectUnit'],
+            'incorrectUnit': self.error_handler.format_val_error('unitClassInvalidUnit', tag=test_strings['incorrectUnit'],
                                               unit_class_units=",".join(sorted(legal_time_units))),
-            'incorrectPluralUnit': format_val_error('unitClassInvalidUnit', tag=test_strings['incorrectPluralUnit'],
+            'incorrectPluralUnit': self.error_handler.format_val_error('unitClassInvalidUnit', tag=test_strings['incorrectPluralUnit'],
                                                     unit_class_units=",".join(sorted(legal_freq_units))),
-            'incorrectSymbolCapitalizedUnit': format_val_error('unitClassInvalidUnit',
+            'incorrectSymbolCapitalizedUnit': self.error_handler.format_val_error('unitClassInvalidUnit',
                                                                tag=test_strings['incorrectSymbolCapitalizedUnit'],
                                                                unit_class_units=",".join(sorted(legal_freq_units))),
-            'incorrectSymbolCapitalizedUnitModifier': format_val_error('unitClassInvalidUnit', tag=test_strings[
+            'incorrectSymbolCapitalizedUnitModifier': self.error_handler.format_val_error('unitClassInvalidUnit', tag=test_strings[
                 'incorrectSymbolCapitalizedUnitModifier'],
                                                                        unit_class_units=",".join(
                                                                             sorted(legal_freq_units))),
             'notRequiredNumber': [],
             'notRequiredScientific': [],
             'properTime': [],
-            'invalidTime': format_val_error('unitClassInvalidUnit', tag=test_strings['invalidTime'],
+            'invalidTime': self.error_handler.format_val_error('unitClassInvalidUnit', tag=test_strings['invalidTime'],
                                             unit_class_units=",".join(sorted(legal_clock_time_units)))
         }
         self.validator_semantic(test_strings, expected_results, expected_issues, True)
@@ -461,8 +462,8 @@ def test_no_duplicates(self):
             'noDuplicate': True
         }
         expected_issues = {
-            'topLevelDuplicate': format_val_error('duplicateTag', tag='Event/Category/Sensory presentation'),
-            'groupDuplicate': format_val_error('duplicateTag', tag='Event/Category/Sensory presentation'),
+            'topLevelDuplicate': self.error_handler.format_val_error('duplicateTag', tag='Event/Category/Sensory presentation'),
+            'groupDuplicate': self.error_handler.format_val_error('duplicateTag', tag='Event/Category/Sensory presentation'),
             'legalDuplicate': [],
             'noDuplicate': []
         }
@@ -482,7 +483,7 @@ def test_multiple_copies_unique_tags(self):
         }
         expected_issues = {
             'legal': [],
-            'multipleDesc': format_val_error('multipleUniqueTags', tag_prefix='Event/Description')
+            'multipleDesc': self.error_handler.format_val_error('multipleUniqueTags', tag_prefix='Event/Description')
         }
         self.validator_semantic(test_strings, expected_results, expected_issues, True)
 
@@ -518,12 +519,12 @@ def test_includes_all_required_tags(self):
         }
         expected_issues = {
             'complete': [],
-            'missingLabel': format_val_warning('requiredPrefixMissing', tag_prefix='Event/Label'),
-            'missingCategory': format_val_warning('requiredPrefixMissing', tag_prefix='Event/Category'),
-            'missingDescription': format_val_warning('requiredPrefixMissing', tag_prefix='Event/Description'),
-            'missingAllRequired': format_val_warning('requiredPrefixMissing',
-                                                     tag_prefix='Event/Label') + format_val_warning(
-                'requiredPrefixMissing', tag_prefix='Event/Category') + format_val_warning('requiredPrefixMissing',
+            'missingLabel': self.error_handler.format_val_warning('requiredPrefixMissing', tag_prefix='Event/Label'),
+            'missingCategory': self.error_handler.format_val_warning('requiredPrefixMissing', tag_prefix='Event/Category'),
+            'missingDescription': self.error_handler.format_val_warning('requiredPrefixMissing', tag_prefix='Event/Description'),
+            'missingAllRequired': self.error_handler.format_val_warning('requiredPrefixMissing',
+                                                     tag_prefix='Event/Label') + self.error_handler.format_val_warning(
+                'requiredPrefixMissing', tag_prefix='Event/Category') + self.error_handler.format_val_warning('requiredPrefixMissing',
                                                                                            tag_prefix='Event/Description'),
         }
         self.validator_semantic(test_strings, expected_results, expected_issues, True)
@@ -560,7 +561,7 @@ def test_no_more_than_two_tildes(self):
             'noTildeGroup': [],
             'oneTildeGroup': [],
             'twoTildeGroup': [],
-            'invalidTildeGroup': format_val_error('tooManyTildes',
+            'invalidTildeGroup': self.error_handler.format_val_error('tooManyTildes',
                                                   tag='(Participant/ID 1 ~ Participant/Effect/Visual '
                                                        '~ Item/Object/Vehicle/Car,'
                                                        ' Item/ID/RedCar,'
@@ -583,6 +584,7 @@ class TestHed3(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
+        cls.error_handler = error_reporter.ErrorHandler()
         hed_xml = os.path.join(os.path.dirname(os.path.abspath(__file__)), cls.schema_file)
         cls.hed_schema = HedSchema(hed_xml)
         cls.syntactic_tag_validator = TagValidator(cls.hed_schema, check_for_warnings=False,
@@ -593,13 +595,13 @@ def setUpClass(cls):
                                                   run_semantic_validation=True)
         cls.semantic_warning_tag_validator = TagValidator(cls.hed_schema, check_for_warnings=True,
                                                           run_semantic_validation=True)
-        cls.syntactic_hed_input_reader = HedValidator("Event/Category")
+        cls.syntactic_hed_input_reader = HedValidator()
         cls.syntactic_hed_input_reader._tag_validator = cls.syntactic_tag_validator
-        cls.syntactic_warning_hed_input_reader = HedValidator("Event/Category")
+        cls.syntactic_warning_hed_input_reader = HedValidator()
         cls.syntactic_warning_hed_input_reader._tag_validator = cls.syntactic_warning_tag_validator
-        cls.semantic_hed_input_reader = HedValidator("Event/Category")
+        cls.semantic_hed_input_reader = HedValidator()
         cls.semantic_hed_input_reader._tag_validator = cls.semantic_tag_validator
-        cls.semantic_warning_hed_input_reader = HedValidator("Event/Category")
+        cls.semantic_warning_hed_input_reader = HedValidator()
         cls.semantic_warning_hed_input_reader._tag_validator = cls.semantic_warning_tag_validator
 
     def validator_base(self, test_strings, expected_results, expected_issues, test_function):
@@ -655,9 +657,9 @@ def test_exist_in_schema(self):
             'takesValue': [],
             'full': [],
             'extensionsAllowed': [],
-            'leafExtension': format_val_error('invalidTag', tag=test_strings['leafExtension']),
-            'nonExtensionsAllowed': format_val_error('invalidTag', tag=test_strings['nonExtensionsAllowed']),
-            'illegalComma': format_val_error('extraCommaOrInvalid', previous_tag='Label/This is a label',
+            'leafExtension': self.error_handler.format_val_error('invalidTag', tag=test_strings['leafExtension']),
+            'nonExtensionsAllowed': self.error_handler.format_val_error('invalidTag', tag=test_strings['nonExtensionsAllowed']),
+            'illegalComma': self.error_handler.format_val_error('extraCommaOrInvalid', previous_tag='Label/This is a label',
                                              tag='This/Is/Not/A/Tag')
         }
         self.validator_semantic(test_strings, expected_results, expected_issues, False)
@@ -682,7 +684,7 @@ def test_proper_capitalization(self):
             'camelCase': [],
             'takesValue': [],
             'numeric': [],
-            'lowercase': format_val_warning('capitalization', tag=test_strings['lowercase'])
+            'lowercase': self.error_handler.format_val_warning('capitalization', tag=test_strings['lowercase'])
         }
         self.validator_syntactic(test_strings, expected_results, expected_issues, True)
 
@@ -697,7 +699,7 @@ def test_child_required(self):
         }
         expected_issues = {
             'hasChild': [],
-            'missingChild': format_val_error('childRequired', tag=test_strings['missingChild'])
+            'missingChild': self.error_handler.format_val_error('childRequired', tag=test_strings['missingChild'])
         }
         self.validator_semantic(test_strings, expected_results, expected_issues, True)
 
@@ -723,13 +725,13 @@ def test_required_units(self):
         legal_clock_time_units = ['hour:min', 'hour:min:sec']
         expected_issues = {
             'hasRequiredUnit': [],
-            'missingRequiredUnit': format_val_warning('unitClassDefaultUsed', tag=test_strings['missingRequiredUnit'],
+            'missingRequiredUnit': self.error_handler.format_val_warning('unitClassDefaultUsed', tag=test_strings['missingRequiredUnit'],
                                                       default_unit='s'),
             'notRequiredNoNumber': [],
             'notRequiredNumber': [],
             'notRequiredScientific': [],
             # 'timeValue': [],
-            # 'invalidTimeValue':format_val_error('unitClassInvalidUnit', tag=test_strings['invalidTimeValue'],
+            # 'invalidTimeValue':self.error_handler.format_val_error('unitClassInvalidUnit', tag=test_strings['invalidTimeValue'],
             #                                     unit_class_units=",".join(sorted(legal_clock_time_units))),
         }
         self.validator_semantic(test_strings, expected_results, expected_issues, True)
@@ -779,21 +781,21 @@ def test_correct_units(self):
             'correctNoPluralUnit': [],
             'correctNonSymbolCapitalizedUnit': [],
             'correctSymbolCapitalizedUnit': [],
-            'incorrectUnit': format_val_error('unitClassInvalidUnit', tag=test_strings['incorrectUnit'],
+            'incorrectUnit': self.error_handler.format_val_error('unitClassInvalidUnit', tag=test_strings['incorrectUnit'],
                                               unit_class_units=",".join(sorted(legal_time_units))),
-            'incorrectPluralUnit': format_val_error('unitClassInvalidUnit', tag=test_strings['incorrectPluralUnit'],
+            'incorrectPluralUnit': self.error_handler.format_val_error('unitClassInvalidUnit', tag=test_strings['incorrectPluralUnit'],
                                                     unit_class_units=",".join(sorted(legal_freq_units))),
-            'incorrectSymbolCapitalizedUnit': format_val_error('unitClassInvalidUnit',
+            'incorrectSymbolCapitalizedUnit': self.error_handler.format_val_error('unitClassInvalidUnit',
                                                                tag=test_strings['incorrectSymbolCapitalizedUnit'],
                                                                unit_class_units=",".join(sorted(legal_freq_units))),
-            'incorrectSymbolCapitalizedUnitModifier': format_val_error('unitClassInvalidUnit', tag=test_strings[
+            'incorrectSymbolCapitalizedUnitModifier': self.error_handler.format_val_error('unitClassInvalidUnit', tag=test_strings[
                 'incorrectSymbolCapitalizedUnitModifier'],
                                                                        unit_class_units=",".join(
                                                                             sorted(legal_freq_units))),
             'notRequiredNumber': [],
             'notRequiredScientific': [],
             # 'properTime': [],
-            # 'invalidTime': format_val_error('unitClassInvalidUnit', tag=test_strings['invalidTime'],
+            # 'invalidTime': self.error_handler.format_val_error('unitClassInvalidUnit', tag=test_strings['invalidTime'],
             #                                 unit_class_units=",".join(sorted(legal_clock_time_units)))
         }
         self.validator_semantic(test_strings, expected_results, expected_issues, True)
@@ -831,7 +833,7 @@ def test_required_units(self):
         }
         expected_issues = {
             'hasRequiredUnit': "",
-            'missingRequiredUnit': format_val_warning('unitClassDefaultUsed', tag=test_strings['missingRequiredUnit'],
+            'missingRequiredUnit': self.error_handler.format_val_warning('unitClassDefaultUsed', tag=test_strings['missingRequiredUnit'],
                                                       default_unit='s'),
             'notRequiredNumber': "",
             'notRequiredScientific': "",
@@ -886,16 +888,16 @@ def test_correct_units(self):
             'correctUnit': [],
             'correctUnitWord': [],
             'correctUnitScientific': [],
-            'incorrectUnit': format_val_error('unitClassInvalidUnit', tag=test_strings['incorrectUnit'],
+            'incorrectUnit': self.error_handler.format_val_error('unitClassInvalidUnit', tag=test_strings['incorrectUnit'],
                                               unit_class_units=",".join(sorted(legal_time_units))),
-            'incorrectUnitWord': format_val_error('unitClassInvalidUnit', tag=test_strings['incorrectUnitWord'],
+            'incorrectUnitWord': self.error_handler.format_val_error('unitClassInvalidUnit', tag=test_strings['incorrectUnitWord'],
                                                   unit_class_units=",".join(sorted(legal_time_units))),
-            'incorrectPrefix': format_val_error('unitClassInvalidUnit', tag=test_strings['incorrectPrefix'],
+            'incorrectPrefix': self.error_handler.format_val_error('unitClassInvalidUnit', tag=test_strings['incorrectPrefix'],
                                                 unit_class_units=",".join(sorted(legal_time_units))),
             'notRequiredNumber': [],
             'notRequiredScientific': [],
             'properTime': [],
-            'invalidTime': format_val_error('unitClassInvalidUnit', tag=test_strings['invalidTime'],
+            'invalidTime': self.error_handler.format_val_error('unitClassInvalidUnit', tag=test_strings['invalidTime'],
                                             unit_class_units=",".join(sorted(legal_time_units)))
         }
         self.validator_semantic(test_strings, expected_results, expected_issues, True)
diff --git a/hedweb/tests/test_validation.py b/hedweb/tests/test_dictionary.py
similarity index 97%
rename from hedweb/tests/test_validation.py
rename to hedweb/tests/test_dictionary.py
index 0a74afff3..1e9b1409f 100644
--- a/hedweb/tests/test_validation.py
+++ b/hedweb/tests/test_dictionary.py
@@ -32,7 +32,7 @@ def setUp(self):
     #         self.app = app.test_client()
 
     def test_generate_input_arguments_from_validation_form(self):
-        self.assertTrue(1, "Testing generate_arguments_from_validation_form")
+        self.assertTrue(1, "Testing generate_arguments_from_dictionary_form")
 
     def test_generate_spreadsheet_validation_filename(self):
         self.assertTrue(1, "Testing generate_dictionary_validation_filename")
diff --git a/hedweb/tests/test_events.py b/hedweb/tests/test_events.py
new file mode 100644
index 000000000..4b6e8b61d
--- /dev/null
+++ b/hedweb/tests/test_events.py
@@ -0,0 +1,42 @@
+import unittest
+# from flask import current_app, jsonify, Response
+# from hed.web.utils import app_config
+# from hed.web.validation import generate_dictionary_validation_filename
+# from hed.web.app_factory import AppFactory
+# from hed.web.constants import file_constants, spreadsheet_constants
+import os
+
+# app = AppFactory.create_app('config.TestConfig')
+# with app.app_context():
+#     from hed.web import web_utils
+#     from hed.web.routes import route_blueprint
+#
+#     app.register_blueprint(route_blueprint, url_prefix=app.config['URL_PREFIX'])
+#     web_utils.create_upload_directory(app.config['UPLOAD_FOLDER'])
+
+
+class Test(unittest.TestCase):
+    def setUp(self):
+        print("Stuff")
+        # self.create_test_app()
+        # self.app = app.app.test_client()
+        # self.major_version_key = 'major_versions'
+        # self.hed_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED.xml')
+        # self.tsv_file1 = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/tsv_file1.txt')
+
+    # def create_test_app(self):
+    #     app = AppFactory.create_app('config.TestConfig')
+    #     with app.app_context():
+    #         from hed.web.routes import route_blueprint
+    #         app.register_blueprint(route_blueprint)
+    #         self.app = app.test_client()
+
+    def test_generate_input_from_events_form(self):
+        self.assertTrue(1, "Testing generate_input_from_events_form")
+
+    def test_report_events_validation_status(self):
+        self.assertTrue(1, "report_events_validation_status")
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/hedweb/tests/test_routes.py b/hedweb/tests/test_routes.py
index f81fff8b7..f2ac8efd7 100644
--- a/hedweb/tests/test_routes.py
+++ b/hedweb/tests/test_routes.py
@@ -53,46 +53,50 @@ def test_download_file_in_upload_directory(self):
         print(response.status_code)
         # self.assertEqual(response.status_code, 204, "Dummy file should be deleted")
 
-    # def test_get_duplicate_tag_results(self):
-    #     response = self.app.get('/schema-duplicate-tag-results')
-    #     self.assertEqual(response.status_code, 404)
-    #
-    # def test_get_eeg_events_validation_results(self):
-    #     response = self.app.get('/eeg-validation-submit')
-    #     self.assertEqual(response.status_code, 404)
-    #
-    # def test_get_hed_version_in_file(self):
-    #     response = self.app.post('/get-hed-version')
-    #     self.assertEqual(response.status_code, 400)
-    #
-    # def test_get_major_hed_versions(self):
-    #     response = self.app.post('/get-hed-major-versions')
-    #     self.assertEqual(response.status_code, 405)
-    # import hed.web.constants.common_constants as constants
-    # from hed.web.web_utils import find_major_hed_versions
-    # hed_info = find_major_hed_versions()
-    # self.assertTrue(constants.HED_MAJOR_VERSIONS in hed_info, "The information has key hed-major-versions")
-    # self.assertTrue('7.1.2' in hed_info[constants.HED_MAJOR_VERSIONS], "7.1.2 is a major versions")
-    #
-    # def test_get_major_hed_versions(self):
-    #     response = self.app.post('/get-hed-major-versions')
-    #     self.assertEqual(response.status_code, 405)
-    #
-    # def test_get_schema_conversion_results(self):
-    #     response = self.app.post('/schema-conversion-submit')
-    #     self.assertEqual(response.status_code, 400)
-    #
-    # def test_get_spreadsheet_columns_info(self):
-    #     response = self.app.post('/get-spreadsheet-columns-info')
-    #     self.assertEqual(response.status_code, 400)
-    #
-    # def test_get_validation_results(self):
-    #     response = self.app.post('/validation-submit')
-    #     self.assertEqual(response.status_code, 400)
-    #
-    # def test_get_worksheets_info(self):
-    #     response = self.app.post('/get-worksheets-info')
-    #     self.assertEqual(response.status_code, 400)
+    def test_get_dictionary_validation_results(self):
+        response = self.app.get('/dictionary-validation-submit')
+        self.assertEqual(response.status_code, 404)
+
+    def test_get_events_validation_results(self):
+        response = self.app.get('/events-validation-submit')
+        self.assertEqual(response.status_code, 404)
+
+    def test_get_hed_services_results(self):
+        response = self.app.get('/hed-services-submit')
+        self.assertEqual(response.status_code, 404)
+
+    def test_get_hed_version(self):
+        response = self.app.post('/get-hed-version')
+        self.assertEqual(response.status_code, 400)
+
+    def test_get_major_hed_versions(self):
+        response = self.app.post('/get-hed-major-versions')
+        self.assertEqual(response.status_code, 405)
+        # import hed.web.constants.common_constants as constants
+        # from hed.web.web_utils import find_major_hed_versions
+        # hed_info = find_major_hed_versions()
+        # self.assertTrue(constants.HED_MAJOR_VERSIONS in hed_info, "The information has key hed-major-versions")
+        # self.assertTrue('7.1.2' in hed_info[constants.HED_MAJOR_VERSIONS], "7.1.2 is a major versions")
+
+    def test_get_schema_compliance_check_results(self):
+        response = self.app.post('/schema-compliance-check-submit')
+        self.assertEqual(response.status_code, 400)
+
+    def test_get_schema_conversion_results(self):
+        response = self.app.post('/schema-conversion-submit')
+        self.assertEqual(response.status_code, 400)
+
+    def test_get_spreadsheet_columns_info(self):
+        response = self.app.post('/get-spreadsheet-columns-info')
+        self.assertEqual(response.status_code, 400)
+
+    def test_get_spreadsheet_validation_results(self):
+        response = self.app.post('/spreadsheet-validation-submit')
+        self.assertEqual(response.status_code, 400)
+
+    def test_get_worksheets_info(self):
+        response = self.app.post('/get-worksheets-info')
+        self.assertEqual(response.status_code, 400)
 
     def test_render_additional_examples_page(self):
         response = self.app.test.get('/additional-examples')
@@ -102,25 +106,33 @@ def test_render_common_error_page(self):
         response = self.app.test.get('/common-errors')
         self.assertEqual(response.status_code, 200, "The common-errors content page exists")
 
-    # def test_render_eeg_validation_form(self):
-    #     response = self.app.get('/eeg-validation')
-    #     self.assertEqual(response.status_code, 200)
-    #
-    # def test_render_home_page(self):
-    #     response = self.app.get('/')
-    #     self.assertEqual(response.status_code, 200)
-    #
-    # def test_render_help_page(self):
-    #     response = self.app.get('/hed-tools-help')
-    #     self.assertEqual(response.status_code, 200)
-    #
-    # def test_render_schema_form(self):
-    #     response = self.app.get('/schema')
-    #     self.assertEqual(response.status_code, 200)
-    #
-    # def test_render_validation_form(self):
-    #     response = self.app.get('/validation')
-    #     self.assertEqual(response.status_code, 200)
+    def test_render_dictionary_form(self):
+        response = self.app.get('/dictionary')
+        self.assertEqual(response.status_code, 200)
+
+    def test_render_events_form(self):
+        response = self.app.get('/events')
+        self.assertEqual(response.status_code, 200)
+
+    def test_render_hed_services_form(self):
+        response = self.app.get('/hed-services')
+        self.assertEqual(response.status_code, 200)
+
+    def test_render_help_page(self):
+        response = self.app.get('/hed-tools-help')
+        self.assertEqual(response.status_code, 200)
+
+    def test_render_home_page(self):
+        response = self.app.get('/')
+        self.assertEqual(response.status_code, 200)
+
+    def test_render_schema_form(self):
+        response = self.app.get('/schema')
+        self.assertEqual(response.status_code, 200)
+
+    def test_render_spreadsheet_form(self):
+        response = self.app.get('/spreadsheet')
+        self.assertEqual(response.status_code, 200)
 
 
 if __name__ == '__main__':
diff --git a/hedweb/tests/test_schema.py b/hedweb/tests/test_schema.py
index 028d2c60c..6950cefc8 100644
--- a/hedweb/tests/test_schema.py
+++ b/hedweb/tests/test_schema.py
@@ -4,7 +4,6 @@
 # from hed.web.validation import generate_dictionary_validation_filename
 # from hed.web.app_factory import AppFactory
 # from hed.web.constants import file_constants, spreadsheet_constants
-import os
 
 # app = AppFactory.create_app('config.TestConfig')
 # with app.app_context():
@@ -31,25 +30,18 @@ def setUp(self):
     #         app.register_blueprint(route_blueprint)
     #         self.app = app.test_client()
 
-
-    def test_generate_input_arguments_from_schema_form(self):
+    def test_generate_input_from_schema_form(self):
         self.assertTrue(1, "Testing generate_input_from_schema_form")
 
-    def test_get_schema_conversion_function(self):
+    def test_get_schema_conversion(self):
         self.assertTrue(1, "Testing get_schema_conversion")
 
-    def test_get_uploaded_file_paths_from_schema_form(self):
-        self.assertTrue(1, "Testing get_uploaded_file_paths_from_schema_form")
+    def test_run_schema_compliance_check(self):
+        self.assertTrue(1, "Testing run_schema_compliance_check")
 
     def test_run_schema_conversion(self):
         self.assertTrue(1, "Testing run_schema_conversion")
 
-    def test_run_schema_duplicate_tag_detection(self):
-        self.assertTrue(1, "Testing run_schema_duplicate_tag_detection")
-
-    def test_url_present_in_form(self):
-        self.assertTrue(1, "Testing form_has_hed_url")
-
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/hedweb/tests/test_services.py b/hedweb/tests/test_services.py
new file mode 100644
index 000000000..1e9b1409f
--- /dev/null
+++ b/hedweb/tests/test_services.py
@@ -0,0 +1,62 @@
+import unittest
+# from flask import current_app, jsonify, Response
+# from hed.web.utils import app_config
+# from hed.web.validation import generate_dictionary_validation_filename
+# from hed.web.app_factory import AppFactory
+# from hed.web.constants import file_constants, spreadsheet_constants
+import os
+
+# app = AppFactory.create_app('config.TestConfig')
+# with app.app_context():
+#     from hed.web import web_utils
+#     from hed.web.routes import route_blueprint
+#
+#     app.register_blueprint(route_blueprint, url_prefix=app.config['URL_PREFIX'])
+#     web_utils.create_upload_directory(app.config['UPLOAD_FOLDER'])
+
+
+class Test(unittest.TestCase):
+    def setUp(self):
+        print("Stuff")
+        # self.create_test_app()
+        # self.app = app.app.test_client()
+        # self.major_version_key = 'major_versions'
+        # self.hed_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED.xml')
+        # self.tsv_file1 = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/tsv_file1.txt')
+
+    # def create_test_app(self):
+    #     app = AppFactory.create_app('config.TestConfig')
+    #     with app.app_context():
+    #         from hed.web.routes import route_blueprint
+    #         app.register_blueprint(route_blueprint)
+    #         self.app = app.test_client()
+
+    def test_generate_input_arguments_from_validation_form(self):
+        self.assertTrue(1, "Testing generate_arguments_from_dictionary_form")
+
+    def test_generate_spreadsheet_validation_filename(self):
+        self.assertTrue(1, "Testing generate_dictionary_validation_filename")
+        # dictionary_filename = 'abc.xls'
+        # expected_spreadsheet_filename = 'validated_' + dictionary_filename.rsplit('.')[0] + '.txt'
+        # validation_file_name = generate_dictionary_validation_filename(dictionary_filename, worksheet_name='')
+        # self.assertTrue(validation_file_name)
+        # self.assertEqual(expected_spreadsheet_filename, validation_file_name)
+
+    def test_get_uploaded_file_paths_from_forms(self):
+        self.assertTrue(1, "Testing get_uploaded_file_paths_from_forms")
+
+    def test_report_eeg_events_validation_status(self):
+        self.assertTrue(1, "Testing report_eeg_events_validation_status")
+
+    def test_report_spreadsheet_validation_status(self):
+        self.assertTrue(1, "Testing report_dictionary_validation_status")
+
+    def test_save_validation_issues_to_file_in_upload_folder(self):
+        self.assertTrue(1, "Testing save_issues_to_upload_folder")
+
+    def test_validate_spreadsheet(self):
+        self.assertTrue(1, "Testing validate_dictionary")
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/hedweb/tests/test_spreadsheet.py b/hedweb/tests/test_spreadsheet.py
new file mode 100644
index 000000000..282068243
--- /dev/null
+++ b/hedweb/tests/test_spreadsheet.py
@@ -0,0 +1,45 @@
+import unittest
+# from flask import current_app, jsonify, Response
+# from hed.web.utils import app_config
+# from hed.web.validation import generate_dictionary_validation_filename
+# from hed.web.app_factory import AppFactory
+# from hed.web.constants import file_constants, spreadsheet_constants
+import os
+
+# app = AppFactory.create_app('config.TestConfig')
+# with app.app_context():
+#     from hed.web import web_utils
+#     from hed.web.routes import route_blueprint
+#
+#     app.register_blueprint(route_blueprint, url_prefix=app.config['URL_PREFIX'])
+#     web_utils.create_upload_directory(app.config['UPLOAD_FOLDER'])
+
+
+class Test(unittest.TestCase):
+    def setUp(self):
+        print("Stuff")
+        # self.create_test_app()
+        # self.app = app.app.test_client()
+        # self.major_version_key = 'major_versions'
+        # self.hed_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED.xml')
+        # self.tsv_file1 = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/tsv_file1.txt')
+
+    # def create_test_app(self):
+    #     app = AppFactory.create_app('config.TestConfig')
+    #     with app.app_context():
+    #         from hed.web.routes import route_blueprint
+    #         app.register_blueprint(route_blueprint)
+    #         self.app = app.test_client()
+
+    def test_generate_input_from_validation_form(self):
+        self.assertTrue(1, "Testing generate_input_from_validation_form")
+
+    def test_report_spreadsheet_validation_status(self):
+        self.assertTrue(1, "Testing report_spreadsheet_validation_status")
+
+    def test_validate_spreadsheet(self):
+        self.assertTrue(1, "Testing validate_spreadsheet")
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/hedweb/tests/test_utils.py b/hedweb/tests/test_utils.py
index 8bb86a75c..a7924ef67 100644
--- a/hedweb/tests/test_utils.py
+++ b/hedweb/tests/test_utils.py
@@ -26,8 +26,8 @@ def tearDownClass(cls):
     def test_find_spreadsheet_columns_info(self):
         self.assertTrue(1, "Testing find_spreadsheet_columns_info")
 
-    # def test_get_column_delimiter_based_on_file_extension(self):
-    #     self.assertTrue(1, "Testing get_column_delimiter_based_on_file_extension")
+    def test_get_column_delimiter_based_on_file_extension(self):
+        self.assertTrue(1, "Testing get_column_delimiter_based_on_file_extension")
     #     from hed.web.utils import get_column_delimiter_based_on_file_extension
     #     delimiter = get_column_delimiter_based_on_file_extension('test.tsv')
     #     self.assertEqual('\t', delimiter, ".tsv files should have a tab delimiter")
@@ -46,9 +46,6 @@ def test_get_excel_worksheet_names(self):
         self.assertIn('PVT Events', worksheet_names, "PVT Events is one of the worksheet names")
         self.assertNotIn('Temp', worksheet_names, "Temp is not one of the worksheet names")
 
-    def test_get_optional_form_field(self):
-        self.assertTrue(1, "Testing get_optional_form_field")
-
     def test_get_original_spreadsheet_filename(self):
         self.assertTrue(1, "Testing get_original_filename")
 
@@ -77,6 +74,12 @@ def test_get_spreadsheet_specific_tag_column_indices(self):
         #     self.assertIsInstance(indices, dict)
         #
 
+    def test_get_spreadsheet_other_tag_column_indices(self):
+        self.assertTrue(1, "Testing get_spreadsheet_other_tag_column_indices")
+
+    def test_get_spreadsheet_specific_tag_column_indices(self):
+        self.assertTrue(1, "Testing get_spreadsheet_specific_tag_column_indices")
+
     def test_get_text_file_column_names(self):
         self.assertTrue(1, "Testing get_text_file_column_names")
         # def test_get_text_file_column_names(self):
diff --git a/hedweb/tests/test_web_utils.py b/hedweb/tests/test_web_utils.py
index 5d9392921..817fe53f0 100644
--- a/hedweb/tests/test_web_utils.py
+++ b/hedweb/tests/test_web_utils.py
@@ -98,9 +98,6 @@ def test_find_all_str_indices_in_list(self):
         self.assertIsInstance(indices, list)
         self.assertEqual(expected_indices, indices)
 
-    def test_find_hed_version_in_uploaded_file(self):
-        self.assertTrue(1, "Testing find_hed_version_in_uploaded_file")
-
     def test_generate_download_file_response(self):
         from hed.web.web_utils import generate_download_file_response
         hed_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED.xml')
@@ -122,6 +119,28 @@ def test_generate_download_file_response(self):
             os.remove(temp_file)
         self.assertFalse(os.path.isfile(temp_file), "Dummy has been deleted")
 
+    def test_generate_filename(self):
+        from hed.web.web_utils import generate_filename
+        filename = generate_filename(None, prefix=None, suffix=None, extension=None)
+        self.assertEqual('', filename, "Returns empty when all arguments are none")
+        filename = generate_filename(None, prefix=None, suffix=None, extension='.txt')
+        self.assertEqual('', filename, "Returns empty when basename, prefix, and suffix are None, but extension is not")
+        filename = generate_filename('c:/temp.json', prefix=None, suffix=None, extension='.txt')
+        self.assertEqual('c_temp.txt', filename,
+                         "Returns stripped basename + extension when prefix, and suffix are None")
+        filename = generate_filename('temp.json', prefix='prefix', suffix='suffix', extension='.txt')
+        self.assertEqual('prefix_temp_suffix.txt', filename,
+                         "Returns stripped basename + extension when prefix, and suffix are None")
+        filename = generate_filename(None, prefix='prefix', suffix='suffix', extension='.txt')
+        self.assertEqual('prefix_suffix.txt', filename,
+                         "Returns correct string when no basename")
+        filename = generate_filename('event-strategy-v3_task-matchingpennies_events.json',
+                                     suffix='blech', extension='.txt')
+        self.assertEqual('event-strategy-v3_task-matchingpennies_events_blech.txt', filename,
+                         "Returns correct string when basename with hyphens")
+        filename = generate_filename('HED7.1.2.xml', suffix='blech', extension='.txt')
+        self.assertEqual('HED7.1.2_blech.txt', filename, "Returns correct string when basename has periods")
+
     def test_get_hed_path_from_pull_down(self):
         from hed.web.constants import common_constants
         from hed.web.web_utils import get_hed_path_from_pull_down
@@ -135,6 +154,8 @@ def test_get_hed_path_from_pull_down(self):
         # self.assertTrue(hed_file_path,
         #                 'When hed-version is in form get_hed_path_from_pull_down returns a hed_file_path')
 
+    def test_get_optional_form_field(self):
+        self.assertTrue(1, "Testing get_optional_form_field")
 
     def test_get_uploaded_file_path_from_form(self):
         self.assertTrue(1, "Testingget_uploaded_file_path_from_form")
@@ -145,6 +166,9 @@ def test_handle_http_error(self):
 
         self.assertTrue(1, "Testing handle_http_error")
 
+    def test_find_hed_version_in_uploaded_file(self):
+            self.assertTrue(1, "Testing find_hed_version_in_uploaded_file")
+
     def test_save_file_to_upload_folder(self):
         from hed.web.web_utils import save_file_to_upload_folder, app_config
         temp_name = save_file_to_upload_folder('')
diff --git a/hedweb/tests/testing_dict_val.py b/hedweb/tests/testing_dict_val.py
index 392965cd9..18d636bcd 100644
--- a/hedweb/tests/testing_dict_val.py
+++ b/hedweb/tests/testing_dict_val.py
@@ -10,6 +10,6 @@
     json_file = ColumnDefGroup(json_filename)
     # Print all the errors from the json file
     errors = json_file.validate_entries(hed_dictionary)
-    for error in errors.values():
-        for sub_error in error:
-            print(sub_error["message"])
\ No newline at end of file
+    for error in errors:
+        x = error
+        print(error["message"])
\ No newline at end of file
diff --git a/hedweb/tests/testing_dups.py b/hedweb/tests/testing_dups.py
index e10a63b99..12c65d9a5 100644
--- a/hedweb/tests/testing_dups.py
+++ b/hedweb/tests/testing_dups.py
@@ -3,7 +3,7 @@
 from urllib.parse import urlparse
 from shutil import move
 from hed.schema import constants
-from hed.util.file_util import delete_file_if_it_exist, url_to_file, get_file_extension, write_text_iter_to_file
+from hed.util.file_util import delete_file_if_it_exists, url_to_file, get_file_extension, write_text_iter_to_file
 
 if __name__ == '__main__':
     # hed_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED.xml')
@@ -11,8 +11,8 @@
     # output_location = result_dict[constants.HED_OUTPUT_LOCATION_KEY]
     # move(output_location, "duplicate_tags_in_HEDXML.txt")
     hed_file_path = ''
-    delete_file_if_it_exist(hed_file_path)
+    delete_file_if_it_exists(hed_file_path)
     hed_file_path = None
-    delete_file_if_it_exist(hed_file_path)
+    delete_file_if_it_exists(hed_file_path)
     url = urlparse('')
     print(url)
\ No newline at end of file