Repository: oasis-open/cti-stix-validator
Pull request #36 (2017-09-08 00:01:18)

Changes to enable validation of files with lists of objects
(I felt like I was stomping over the code more when I wrote this; that's why the branch name is "*_stomped".)

So this is a first cut.  I want to add more comments inline with the code too, within this PR, to help explain changes.


Patch:
diff --git a/stix2validator/__init__.py b/stix2validator/__init__.py
index 8ae7b34..01913ce 100644
--- a/stix2validator/__init__.py
+++ b/stix2validator/__init__.py
@@ -4,6 +4,6 @@
 from .errors import ValidationError
 from .output import print_results
 from .util import ValidationOptions
-from .validator import (run_validation, validate_file, validate_instance,
-                        validate_string)
+from .validator import (run_validation, validate, validate_file,
+                        validate_parsed_json, validate_string)
 from .version import __version__
diff --git a/stix2validator/codes.py b/stix2validator/codes.py
index 1ddb65e..6fbea51 100644
--- a/stix2validator/codes.py
+++ b/stix2validator/codes.py
@@ -1,8 +1,6 @@
 """Exit status codes
 """
 
-from six import itervalues
-
 #: Execution finished successfully. All STIX documents were valid for all user-
 #: specified validation scenarios.
 EXIT_SUCCESS = 0x0
@@ -30,9 +28,10 @@ def get_code(results):
     """
     status = EXIT_SUCCESS
 
-    for result in itervalues(results):
-        error = result.errors
-        fatal = result.fatal
+    for file_result in results:
+        error = any(object_result.errors for object_result in file_result.object_results)
+
+        fatal = file_result.fatal
 
         if error:
             status |= EXIT_SCHEMA_INVALID
diff --git a/stix2validator/output.py b/stix2validator/output.py
index a311b68..41fd363 100644
--- a/stix2validator/output.py
+++ b/stix2validator/output.py
@@ -1,7 +1,7 @@
+import operator
 import sys
 
 from colorama import Fore, Style, init
-from six import iteritems
 
 from . import codes
 
@@ -106,12 +106,12 @@ def print_schema_results(results, level=0):
     """Prints JSON Schema validation errors to stdout.
 
     Args:
-        results: An instance of ValidationResults.
+        results: An instance of ObjectValidationResults.
         level: The level at which to print the results.
 
     """
     for error in results.errors:
-        print_level(_RED + "[X] %s", level + 1, error)
+        print_level(_RED + "[X] %s", level, error)
 
 
 def print_warning_results(results, level=0):
@@ -120,7 +120,7 @@ def print_warning_results(results, level=0):
     marker = _YELLOW + "[!] "
 
     for warning in results.warnings:
-        print_level(marker + "Warning: %s", level + 1, warning)
+        print_level(marker + "Warning: %s", level, warning)
 
 
 def print_horizontal_rule():
@@ -142,30 +142,26 @@ def print_results(results):
     """Prints `results` (the results of validation) to stdout.
 
     Args:
-        results: A dictionary of ValidationResults instances. The key is the
-            file path to the validated document.
+        results: A list of FileValidationResults instances.
 
     """
-    if not hasattr(results, 'items'):
-        results_arr = {'': results}
-        results = results_arr
-
-    level = 0
-    for fn, result in sorted(iteritems(results)):
+    for file_result in sorted(results, key=operator.attrgetter("filepath")):
         print_horizontal_rule()
-        print_level("[-] Results for: %s", level, fn)
+        print_level("[-] Results for: %s", 0, file_result.filepath)
 
-        if result.is_valid:
+        if file_result.is_valid:
             marker = _GREEN + "[+]"
             verdict = "Valid"
         else:
             marker = _RED + "[X]"
             verdict = "Invalid"
-        print_level("%s STIX JSON: %s", level, marker, verdict)
-
-        if result.warnings:
-            print_warning_results(result, level)
-        if result.errors:
-            print_schema_results(result, level)
-        if result.fatal:
-            print_fatal_results(result.fatal, level)
+        print_level("%s STIX JSON: %s", 0, marker, verdict)
+
+        for object_result in file_result.object_results:
+            if object_result.warnings:
+                print_warning_results(object_result, 1)
+            if object_result.errors:
+                print_schema_results(object_result, 1)
+
+        if file_result.fatal:
+            print_fatal_results(file_result.fatal, 1)
diff --git a/stix2validator/scripts/stix2_validator.py b/stix2validator/scripts/stix2_validator.py
index b56cbe3..50380cb 100644
--- a/stix2validator/scripts/stix2_validator.py
+++ b/stix2validator/scripts/stix2_validator.py
@@ -5,10 +5,16 @@
 
 import argparse
 from argparse import RawDescriptionHelpFormatter
+import datetime
+import errno
 import logging
+import os
 import sys
 import textwrap
 
+from appdirs import AppDirs
+import requests_cache
+
 from stix2validator import (ValidationError, ValidationOptions, codes, output,
                             print_results, run_validation)
 
@@ -244,6 +250,38 @@ def _get_arg_parser(is_script=True):
     return parser
 
 
+def init_requests_cache(refresh_cache=False):
+    """
+    Initializes a cache which the ``requests`` library will consult for
+    responses, before making network requests.
+
+    :param refresh_cache: Whether the cache should be cleared out
+    """
+    # Cache data from external sources; used in some checks
+    dirs = AppDirs("stix2-validator", "OASIS")
+    # Create cache dir if doesn't exist
+    try:
+        os.makedirs(dirs.user_cache_dir)
+    except OSError as e:
+        if e.errno != errno.EEXIST:
+            raise
+    requests_cache.install_cache(
+        cache_name=os.path.join(dirs.user_cache_dir, 'py{}cache'.format(
+            sys.version_info[0])),
+        expire_after=datetime.timedelta(weeks=1))
+
+    if refresh_cache:
+        clear_requests_cache()
+
+
+def clear_requests_cache():
+    """
+    Clears all cached responses.
+    """
+    now = datetime.datetime.utcnow()
+    requests_cache.get_cache().remove_old_entries(now)
+
+
 def main():
     # Parse command line arguments
     parser = _get_arg_parser()
@@ -255,12 +293,18 @@ def main():
         # Set the output level (e.g., quiet vs. verbose)
         output.set_level(options.verbose)
 
+        if not options.no_cache:
+            init_requests_cache(options.refresh_cache)
+
         # Validate input documents
         results = run_validation(options)
 
         # Print validation results
         print_results(results)
 
+        if not options.no_cache and options.clear_cache:
+            clear_requests_cache()
+
         # Determine exit status code and exit.
         code = codes.get_code(results)
         sys.exit(code)
diff --git a/stix2validator/validator.py b/stix2validator/validator.py
index 35a7aca..a54b053 100644
--- a/stix2validator/validator.py
+++ b/stix2validator/validator.py
@@ -2,19 +2,14 @@
 """
 
 from collections import Iterable
-import datetime
-import errno
-import fnmatch
+import io
 from itertools import chain
 import os
-import sys
 
-from appdirs import AppDirs
 from jsonschema import Draft4Validator, RefResolver
 from jsonschema import exceptions as schema_exceptions
-import requests_cache
 import simplejson as json
-from six import iteritems, text_type
+from six import iteritems, string_types, text_type
 
 from . import musts, output, shoulds
 from .errors import (NoJSONFileFoundError, SchemaError, SchemaInvalidError,
@@ -22,62 +17,44 @@ from .errors import (NoJSONFileFoundError, SchemaError, SchemaInvalidError,
 from .util import ValidationOptions
 
 
-class CustomDraft4Validator(Draft4Validator):
-    """Custom validator class for JSON Schema Draft 4.
+def _is_iterable_non_string(val):
+    return hasattr(val, "__iter__") and not isinstance(val, string_types)
 
-    """
-    def __init__(self, schema, types=(), resolver=None, format_checker=None,
-                 options=ValidationOptions()):
-        super(CustomDraft4Validator, self).__init__(schema, types, resolver,
-                                                    format_checker)
-        self.musts_list = musts.list_musts(options)
-        self.shoulds_list = shoulds.list_shoulds(options)
-        self.options = options
-
-    def iter_errors_custom(self, instance, check_musts=True):
-        """Perform additional validation not possible merely with JSON schemas.
-
-        Args:
-            instance: The STIX object to be validated.
-            check_musts: If True, this function will check against the
-                additional mandatory "MUST" requirements which cannot be
-                enforced by schemas. If False, this function will check against
-                recommended "SHOULD" best practices instead. This function will
-                never check both; to do so call it twice.
-        """
-        # Ensure `instance` is a whole STIX object, not just a property of one
-        if not (type(instance) is dict and 'id' in instance and 'type' in instance):
-            return
 
-        if check_musts:
-            validators = self.musts_list
-        else:
-            validators = self.shoulds_list
+def _is_stix_obj(obj):
+    return isinstance(obj, dict) and 'id' in obj and 'type' in obj
 
-        # Perform validation
-        for v_function in validators:
-            try:
-                result = v_function(instance)
-            except TypeError:
-                result = v_function(instance, self.options)
-            if isinstance(result, Iterable):
-                for x in result:
-                    yield x
-            elif result is not None:
-                yield result
-
-        # Validate any child STIX objects
-        for field in instance:
-            if type(instance[field]) is list:
-                for obj in instance[field]:
-                    for err in self.iter_errors_custom(obj, check_musts):
-                        yield err
 
-    def get_list(self):
-        """Return a combined list of all musts and shoulds this validator will
-        perform, both MUSTs and SHOULDs.
-        """
-        return self.musts_list + self.shoulds_list
+def _iter_errors_custom(instance, checks, options):
+    """Perform additional validation not possible merely with JSON schemas.
+
+    Args:
+        instance: The STIX object to be validated.
+        checks: A sequence of callables which do the checks.  Each callable
+            may be written to accept 1 arg, which is the object to check,
+            or 2 args, which are the object and a ValidationOptions instance.
+        options: ValidationOptions instance with settings affecting how
+            validation should be done.
+    """
+    # Perform validation
+    for v_function in checks:
+        try:
+            result = v_function(instance)
+        except TypeError:
+            result = v_function(instance, options)
+        if isinstance(result, Iterable):
+            for x in result:
+                yield x
+        elif result is not None:
+            yield result
+
+    # Validate any child STIX objects
+    for field in instance:
+        if type(instance[field]) is list:
+            for obj in instance[field]:
+                if _is_stix_obj(obj):
+                    for err in _iter_errors_custom(obj, checks, options):
+                        yield err
 
 
 class BaseResults(object):
@@ -113,8 +90,91 @@ class BaseResults(object):
         return json.dumps(self.as_dict())
 
 
-class ValidationResults(BaseResults):
-    """Results of JSON schema validation.
+class FileValidationResults(BaseResults):
+    """
+    Represents validation results for a file.  This entails potentially
+    several STIX object results, since a file may contain a list of STIX
+    objects.
+    """
+    def __init__(self, is_valid=False, filepath=None, object_results=None, fatal=None):
+        """
+        Initialize this instance.
+        :param is_valid: Whether the overall result is valid
+        :param filepath: Which file was validated
+        :param object_results: Individual object validation results
+        :param fatal: A non-validation-related fatal error
+        """
+        super(FileValidationResults, self).__init__(is_valid)
+        self.filepath = filepath
+        self.object_results = object_results
+        self.fatal = fatal
+
+    def as_dict(self):
+        d = super(FileValidationResults, self).as_dict()
+        d.update(
+            filepath=self.filepath,
+            object_results=[object_result.as_dict() for object_result in self.object_results],
+            fatal=self.fatal.as_dict()
+        )
+
+        return d
+
+    @property
+    def object_result(self):
+        """
+        Get the object result object, assuming there is only one.  Raises
+        an error if there is more than one.
+        :return: The result object
+        :raises ValueError: If there is more than one result
+        """
+        num_obj_results = len(self._object_results)
+
+        if num_obj_results < 1:
+            return None
+        elif num_obj_results < 2:
+            return self._object_results[0]
+        else:
+            raise ValueError("There is more than one result; use 'object_results'")
+
+    @object_result.setter
+    def object_result(self, object_result):
+        """
+        Set the object result to a single value.  If ``object_result`` is not a
+        single value, an error will be raised.
+        :param object_result: The result to set
+        :raises ValueError: if ``object_result`` is not a single value.
+        """
+        if _is_iterable_non_string(object_result):
+            raise ValueError("Can't set \"object_result\" to more than one"
+                             " result; try setting \"object_results\" instead")
+        self._object_results = [object_result]
+
+    @property
+    def object_results(self):
+        """
+        Get all object results.
+        :return: the results
+        """
+        return self._object_results
+
+    @object_results.setter
+    def object_results(self, object_results):
+        """
+        Set the results to an iterable of values.  The values will be collected
+        into a list.  A single value is allowed; it will be converted to a
+        length 1 list.
+        :param object_results: The results to set
+        """
+        if _is_iterable_non_string(object_results):
+            self._object_results = list(object_results)
+        elif object_results is None:
+            self._object_results = []
+        else:
+            self._object_results = [object_results]
+
+
+class ObjectValidationResults(BaseResults):
+    """Results of JSON schema validation for a single STIX object.
 
     Args:
         is_valid: The validation result.
@@ -130,13 +190,10 @@ class ValidationResults(BaseResults):
             otherwise.
 
     """
-    def __init__(self, is_valid=False, errors=None, fatal=None, warnings=None,
-                 fn=None):
-        super(ValidationResults, self).__init__(is_valid)
+    def __init__(self, is_valid=False, errors=None, warnings=None):
+        super(ObjectValidationResults, self).__init__(is_valid)
         self.errors = errors
-        self.fatal = fatal
         self.warnings = warnings
-        self.fn = fn
 
     @property
     def errors(self):
@@ -154,7 +211,7 @@ class ValidationResults(BaseResults):
             self._errors = [SchemaError(value)]
 
     def as_dict(self):
-        """A dictionary representation of the :class:`.ValidationResults`
+        """A dictionary representation of the :class:`.ObjectValidationResults`
         instance.
 
         Keys:
@@ -165,7 +222,7 @@ class ValidationResults(BaseResults):
             A dictionary representation of an instance of this class.
 
         """
-        d = super(ValidationResults, self).as_dict()
+        d = super(ObjectValidationResults, self).as_dict()
 
         if self.errors:
             d['errors'] = [x.as_dict() for x in self.errors]
@@ -277,9 +334,64 @@ def run_validation(options):
     except NoJSONFileFoundError as e:
         output.error(e.message)
 
-    results = {}
-    for fn in files:
-        results[fn] = validate_file(fn, options)
+    results = [validate_file(fn, options) for fn in files]
+
+    return results
+
+
+def validate_parsed_json(obj_json, options=None):
+    """
+    Validate objects from parsed JSON.  This supports a single object, or a
+    list of objects.  If a single object is given, a single result is
+    returned.  Otherwise, a list of results is returned.
+
+    If an error occurs, a ValidationErrorResults instance or list which
+    includes one of these instances, is returned.
+
+    :param obj_json: The parsed json
+    :param options: Validation options
+    :return: An ObjectValidationResults instance, or a list of such.
+    """
+
+    validating_list = isinstance(obj_json, list)
+
+    if not options:
+        options = ValidationOptions()
+
+    results = None
+    try:
+        if validating_list:
+            # Doing it this way instead of using a comprehension means that
+            # initial validation results will be retained, even if a later
+            # exception aborts the sequence.
+            results = []
+            for obj in obj_json:
+                results.append(validate_instance(obj, options))
+        else:
+            results = validate_instance(obj_json, options)
+
+    except SchemaInvalidError as ex:
+        error_result = ObjectValidationResults(is_valid=False,
+                                               errors=[str(ex)])
+        if validating_list:
+            results.append(error_result)
+        else:
+            results = error_result
+
+    return results
+
+
+def validate(in_, options=None):
+    """
+    Validate objects from JSON data in a textual stream.
+
+    :param in_: A textual stream of JSON data.
+    :param options: Validation options
+    :return: An ObjectValidationResults instance, or a list of such.
+    """
+    obj_json = json.load(in_)
+
+    results = validate_parsed_json(obj_json, options)
 
     return results
 
@@ -295,10 +407,10 @@ def validate_file(fn, options=None):
         options: An instance of ``ValidationOptions``.
 
     Returns:
-        An instance of ValidationResults.
+        An instance of FileValidationResults.
 
     """
-    results = ValidationResults(fn=fn)
+    file_results = FileValidationResults(filepath=fn)
     output.info("Performing JSON schema validation on %s" % fn)
 
     if not options:
@@ -306,31 +418,25 @@ def validate_file(fn, options=None):
 
     try:
         with open(fn) as instance_file:
-            instance = json.load(instance_file)
+            file_results.object_results = validate(instance_file, options)
 
-        if options.files:
-            results = validate_instance(instance, options)
-
-    except SchemaInvalidError as ex:
-        results.fatal = ValidationErrorResults(ex)
-        msg = ("File '{fn}' was schema-invalid. No further validation "
-               "will be performed.")
-        output.info(msg.format(fn=fn))
     except Exception as ex:
         if 'Expecting value' in str(ex):
             line_no = str(ex).split()[3]
-            results.fatal = ValidationErrorResults('Invalid JSON input on line'
-                                                   ' %s' % line_no)
+            file_results.fatal = ValidationErrorResults(
+                'Invalid JSON input on line %s' % line_no
+            )
         else:
-            results.fatal = ValidationErrorResults(ex)
+            file_results.fatal = ValidationErrorResults(ex)
+
         msg = ("Unexpected error occurred with file '{fn}'. No further "
                "validation will be performed: {error}")
         output.info(msg.format(fn=fn, error=str(ex)))
 
-    if results.errors or results.fatal:
-        results.is_valid = False
+    file_results.is_valid = all(object_result.is_valid
+                                for object_result in file_results.object_results)
 
-    return results
+    return file_results
 
 
 def validate_string(string, options=None):
@@ -344,40 +450,20 @@ def validate_string(string, options=None):
         options: An instance of ``ValidationOptions``.
 
     Returns:
-        An instance of ValidationResults.
+        An ObjectValidationResults instance, or a list of such.
 
     """
-    results = ValidationResults(fn="input string")
     output.info("Performing JSON schema validation on input string: " + string)
-    instance = json.loads(string)
-
-    if not options:
-        options = ValidationOptions()
-
-    try:
-        results = validate_instance(instance, options)
-    except SchemaInvalidError as ex:
-        results.fatal = ValidationErrorResults(ex)
-        msg = ("String was schema-invalid. No further validation "
-               "will be performed.")
-        output.info(msg.format(string=string))
-
-    if results.errors or results.fatal:
-        results.is_valid = False
-
-    return results
+    stream = io.StringIO(string)
+    return validate(stream, options)
 
 
-def load_validator(schema_path, schema, options, custom=True):
+def load_validator(schema_path, schema):
     """Create a JSON schema validator for the given schema.
 
     Args:
         schema_path: The filename of the JSON schema.
         schema: A Python object representation of the same schema.
-        options: ValidationOptions instance with validation options for this
-            validator.
-        custom: A boolean, True indicating the CustomDraft4Validator should be
-            used, False indicating the default Draft4Validator should be used.
 
     Returns:
         An instance of Draft4Validator.
@@ -390,11 +476,7 @@ def load_validator(schema_path, schema, options, custom=True):
         file_prefix = 'file:'
 
     resolver = RefResolver(file_prefix + schema_path.replace("\\", "/"), schema)
-
-    if custom:
-        validator = CustomDraft4Validator(schema, resolver=resolver, options=options)
-    else:
-        validator = Draft4Validator(schema, resolver=resolver)
+    validator = Draft4Validator(schema, resolver=resolver)
 
     return validator
 
@@ -403,9 +485,10 @@ def find_schema(schema_dir, obj_type):
     """Search the `schema_dir` directory for a schema called `obj_type`.json.
     Return the file path of the first match it finds.
     """
+    schema_filename = obj_type + '.json'
     for root, dirnames, filenames in os.walk(schema_dir):
-        for filename in fnmatch.filter(filenames, obj_type + '.json'):
-            return os.path.join(root, filename)
+        if schema_filename in filenames:
+            return os.path.join(root, schema_filename)
 
 
 def load_schema(schema_path):
@@ -428,8 +511,10 @@ def load_schema(schema_path):
     return schema
 
 
-def object_validate(sdo, options, error_gens):
-    """Validate a single STIX object against its type's schema.
+def _schema_validate(sdo, options):
+    """Set up validation of a single STIX object against its type's schema.
+    This does no actual validation; it just returns generators which must be
+    iterated to trigger the actual generation.
 
     Do not call this function directly; use validate_instance() instead, as it
     calls this one. This function does not perform any custom checks.
@@ -457,8 +542,7 @@ def object_validate(sdo, options, error_gens):
         }
 
     # Don't use custom validator; only check schemas, no additional checks
-    sdo_validator = load_validator(sdo_schema_path, sdo_schema,
-                                   options, custom=False)
+    sdo_validator = load_validator(sdo_schema_path, sdo_schema)
     try:
         sdo_errors = sdo_validator.iter_errors(sdo)
     except schema_exceptions.RefResolutionError:
@@ -472,7 +556,7 @@ def object_validate(sdo, options, error_gens):
             error_prefix = 'unidentifiable object: '
     else:
         error_prefix = ''
-    error_gens.append((sdo_errors, error_prefix))
+    error_gens = [(sdo_errors, error_prefix)]
 
     # Validate each cyber observable object separately
     if sdo['type'] == 'observed-data' and 'objects' in sdo:
@@ -491,8 +575,7 @@ def object_validate(sdo, options, error_gens):
                                              "object's type, nor the base "
                                              "schema (core.json).")
 
-            obj_validator = load_validator(obj_schema_path, obj_schema,
-                                           options, custom=False)
+            obj_validator = load_validator(obj_schema_path, obj_schema)
             try:
                 obj_errors = obj_validator.iter_errors(obj)
             except schema_exceptions.RefResolutionError:
@@ -526,64 +609,24 @@ def validate_instance(instance, options=None):
     if not options:
         options = ValidationOptions()
 
-    # Find and load the schema
-    try:
-        schema_path = find_schema(options.schema_dir, instance['type'])
-        schema = load_schema(schema_path)
-    except (KeyError, TypeError):
-        # Assume a custom object with no schema
-        try:
-            schema_path = find_schema(options.schema_dir, 'core')
-            schema = load_schema(schema_path)
-        except (KeyError, TypeError):
-            raise SchemaInvalidError("Cannot locate a schema for the object's "
-                                     "type, nor the base schema (core.json).")
+    error_gens = []
 
-    # Validate against schemas for specific object types later
-    if instance['type'] == 'bundle':
-        schema['properties']['objects'] = {
-            "objects": {
-                "type": "array",
-                "minItems": 1
-            }
-        }
-    elif instance['type'] == 'observed-data':
-        schema['allOf'][1]['properties']['objects'] = {
-            "objects": {
-                "type": "object",
-                "minProperties": 1
-            }
-        }
+    # Schema validation
+    if instance['type'] == 'bundle' and 'objects' in instance:
+        # Validate each object in a bundle separately
+        for sdo in instance['objects']:
+            error_gens += _schema_validate(sdo, options)
+    else:
+        error_gens += _schema_validate(instance, options)
 
-    # Validate the schema first
-    try:
-        CustomDraft4Validator.check_schema(schema)
-    except schema_exceptions.SchemaError as e:
-        raise SchemaInvalidError('Invalid JSON schema: ' + str(e))
-
-    # Cache data from external sources; used in some checks
-    if not options.no_cache:
-        dirs = AppDirs("stix2-validator", "OASIS")
-        # Create cache dir if doesn't exist
-        try:
-            os.makedirs(dirs.user_cache_dir)
-        except OSError as e:
-            if e.errno != errno.EEXIST:
-                raise
-        requests_cache.install_cache(cache_name=os.path.join(dirs.user_cache_dir, 'py{}cache'.format(sys.version_info[0])),
-                                     expire_after=datetime.timedelta(weeks=1))
-    if options.refresh_cache:
-        now = datetime.datetime.utcnow()
-        requests_cache.get_cache().remove_old_entries(now)
-
-    validator = load_validator(schema_path, schema, options)
+    # Custom validation
+    must_checks = musts.list_musts(options)
+    should_checks = shoulds.list_shoulds(options)
     output.info("Running the following additional checks: %s."
-                % ", ".join(x.__name__ for x in validator.get_list()))
-
-    # Actual validation of JSON document
+                % ", ".join(x.__name__ for x in chain(must_checks, should_checks)))
     try:
-        errors = validator.iter_errors_custom(instance)
-        warnings = validator.iter_errors_custom(instance, False)
+        errors = _iter_errors_custom(instance, must_checks, options)
+        warnings = _iter_errors_custom(instance, should_checks, options)
 
         if options.strict:
             chained_errors = chain(errors, warnings)
@@ -597,21 +640,10 @@ def validate_instance(instance, options=None):
 
     # List of error generators and message prefixes (to denote which object the
     # error comes from)
-    error_gens = [(chained_errors, '')]
-
-    # Validate each object in a bundle separately
-    if instance['type'] == 'bundle' and 'objects' in instance:
-        for sdo in instance['objects']:
-            object_validate(sdo, options, error_gens)
-    else:
-        object_validate(instance, options, error_gens)
-
-    # Clear requests cache if commandline flag was set
-    if options.clear_cache:
-        now = datetime.datetime.utcnow()
-        requests_cache.get_cache().remove_old_entries(now)
+    error_gens += [(chained_errors, '')]
 
-    # Prepare the list of errors
+    # Prepare the list of errors (this actually triggers the custom validation
+    # functions).
     error_list = []
     for gen, prefix in error_gens:
         for error in gen:
@@ -623,5 +655,5 @@ def validate_instance(instance, options=None):
     else:
         valid = True
 
-    return ValidationResults(is_valid=valid, errors=error_list,
-                             warnings=warnings)
+    return ObjectValidationResults(is_valid=valid, errors=error_list,
+                                   warnings=warnings)
diff --git a/tox.ini b/tox.ini
index 806ba11..9f3e67f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -8,8 +8,9 @@ deps =
   pytest
   pytest-cov
   coverage
+changedir={envdir}
 commands =
-  py.test --cov=stix2validator stix2validator/test/ --cov-report term-missing
+  py.test --cov=stix2validator {toxinidir}/stix2validator/test/ --cov-report term-missing
 
 passenv = CI TRAVIS TRAVIS_*
 
@@ -17,6 +18,7 @@ passenv = CI TRAVIS TRAVIS_*
 deps =
   flake8
   pycodestyle
+changedir={toxinidir}
 commands =
   pycodestyle ./stix2validator
   flake8
@@ -24,8 +26,8 @@ commands =
 [testenv:isort-check]
 deps = isort
 commands =
-    isort -rc stix2validator -df
-    isort -rc stix2validator -c
+    isort -rc {toxinidir}/stix2validator -df
+    isort -rc {toxinidir}/stix2validator -c
 
 [pycodestyle]
 max-line-length=160


Test patch:
diff --git a/stix2validator/test/__init__.py b/stix2validator/test/__init__.py
index 6c87468..d4613e1 100644
--- a/stix2validator/test/__init__.py
+++ b/stix2validator/test/__init__.py
@@ -1,7 +1,7 @@
 import os
 import unittest
 
-from .. import ValidationOptions, print_results, validate_instance
+from .. import ValidationOptions, validate_parsed_json
 
 SCHEMA_DIR = os.path.abspath(os.path.dirname(__file__) + "../../schemas")
 
@@ -32,8 +32,7 @@ class ValidatorTest(unittest.TestCase):
         else:
             options = ValidationOptions(schema_dir=SCHEMA_DIR, strict=True,
                                         **kwargs)
-        results = validate_instance(instance, options)
-        print_results(results)
+        results = validate_parsed_json(instance, options)
         self.assertTrue(results.is_valid)
 
     def assertFalseWithOptions(self, instance, **kwargs):
@@ -50,5 +49,5 @@ class ValidatorTest(unittest.TestCase):
         else:
             options = ValidationOptions(schema_dir=SCHEMA_DIR, strict=True,
                                         **kwargs)
-        results = validate_instance(instance, options)
+        results = validate_parsed_json(instance, options)
         self.assertEqual(results.is_valid, False)
diff --git a/stix2validator/test/attack_pattern_tests.py b/stix2validator/test/attack_pattern_tests.py
index 969ab25..34905a7 100644
--- a/stix2validator/test/attack_pattern_tests.py
+++ b/stix2validator/test/attack_pattern_tests.py
@@ -3,10 +3,10 @@ import json
 import unittest
 
 from . import ValidatorTest
-from .. import validate_instance, validate_string
+from .. import validate_parsed_json, validate_string
 
 
-VALID_ATTACK_PATTERN = """
+VALID_ATTACK_PATTERN = u"""
 {
   "type": "attack-pattern",
   "id": "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061",
@@ -35,20 +35,20 @@ class AttackPatternTestCases(ValidatorTest):
         attack_pattern = copy.deepcopy(self.valid_attack_pattern)
         ext_refs = attack_pattern['external_references']
         ext_refs[0]['external_id'] = "CAPEC-abc"
-        results = validate_instance(attack_pattern, self.options)
+        results = validate_parsed_json(attack_pattern, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_external_reference_no_external_id(self):
         attack_pattern = copy.deepcopy(self.valid_attack_pattern)
         ext_refs = attack_pattern['external_references']
         del ext_refs[0]['external_id']
-        results = validate_instance(attack_pattern, self.options)
+        results = validate_parsed_json(attack_pattern, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_invalid_property_prefix(self):
         attack_pattern = copy.deepcopy(self.valid_attack_pattern)
         attack_pattern['x-something'] = "some value"
-        results = validate_instance(attack_pattern, self.options)
+        results = validate_parsed_json(attack_pattern, self.options)
         self.assertEqual(results.is_valid, False)
 
         self.assertFalseWithOptions(attack_pattern, enabled='custom-prefix-lax')
@@ -56,7 +56,7 @@ class AttackPatternTestCases(ValidatorTest):
     def test_invalid_property_prefix_lax(self):
         attack_pattern = copy.deepcopy(self.valid_attack_pattern)
         attack_pattern['x_something'] = "some value"
-        results = validate_instance(attack_pattern, self.options)
+        results = validate_parsed_json(attack_pattern, self.options)
         self.assertEqual(results.is_valid, False)
 
         self.assertTrueWithOptions(attack_pattern, enabled='custom-prefix-lax')
@@ -66,7 +66,7 @@ class AttackPatternTestCases(ValidatorTest):
     def test_valid_property_prefix(self):
         attack_pattern = copy.deepcopy(self.valid_attack_pattern)
         attack_pattern['x_source_something'] = "some value"
-        results = validate_instance(attack_pattern, self.options)
+        results = validate_parsed_json(attack_pattern, self.options)
         self.assertTrue(results.is_valid)
 
     def test_invalid_timestamp(self):
diff --git a/stix2validator/test/bundle_tests.py b/stix2validator/test/bundle_tests.py
index 4cbe3c8..e2e5bc1 100644
--- a/stix2validator/test/bundle_tests.py
+++ b/stix2validator/test/bundle_tests.py
@@ -5,7 +5,7 @@ import unittest
 from . import ValidatorTest
 
 
-VALID_BUNDLE = """
+VALID_BUNDLE = u"""
 {
   "type": "bundle",
   "id": "bundle--44af6c39-c09b-49c5-9de2-394224b04982",
diff --git a/stix2validator/test/custom_obj_tests.py b/stix2validator/test/custom_obj_tests.py
index c6b4faf..35ad5ef 100644
--- a/stix2validator/test/custom_obj_tests.py
+++ b/stix2validator/test/custom_obj_tests.py
@@ -3,10 +3,10 @@ import json
 import unittest
 
 from . import ValidatorTest
-from .. import ValidationError, validate_instance, validate_string
+from .. import ValidationError, validate_parsed_json, validate_string
 
 
-VALID_CUSTOM_OBJECT = """
+VALID_CUSTOM_OBJECT = u"""
 {
   "type": "x-example-com-customobject",
   "id": "x-example-com-customobject--4527e5de-8572-446a-a57a-706f15467461",
@@ -28,36 +28,36 @@ class CustomObjectTestCases(ValidatorTest):
     def test_no_type(self):
         custom_obj = copy.deepcopy(self.valid_custom_object)
         del custom_obj['type']
-        self.assertRaises(ValidationError, validate_instance, custom_obj, self.options)
+        self.assertRaises(ValidationError, validate_parsed_json, custom_obj, self.options)
 
     def test_no_id(self):
         custom_obj = copy.deepcopy(self.valid_custom_object)
         del custom_obj['id']
-        results = validate_instance(custom_obj, self.options)
+        results = validate_parsed_json(custom_obj, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_no_created(self):
         custom_obj = copy.deepcopy(self.valid_custom_object)
         del custom_obj['created']
-        results = validate_instance(custom_obj, self.options)
+        results = validate_parsed_json(custom_obj, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_no_modified(self):
         custom_obj = copy.deepcopy(self.valid_custom_object)
         del custom_obj['modified']
-        results = validate_instance(custom_obj, self.options)
+        results = validate_parsed_json(custom_obj, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_invalid_type_name(self):
         custom_obj = copy.deepcopy(self.valid_custom_object)
         custom_obj['type'] = "corpo_ration"
         custom_obj['id'] = "corpo_ration--4527e5de-8572-446a-a57a-706f15467461"
-        results = validate_instance(custom_obj, self.options)
+        results = validate_parsed_json(custom_obj, self.options)
         self.assertEqual(results.is_valid, False)
 
         custom_obj['type'] = "corpor@tion"
         custom_obj['id'] = "corpor@tion--4527e5de-8572-446a-a57a-706f15467461"
-        results = validate_instance(custom_obj, self.options)
+        results = validate_parsed_json(custom_obj, self.options)
         self.assertEqual(results.is_valid, False)
 
         self.assertFalseWithOptions(custom_obj, enabled='custom-prefix-lax')
@@ -67,7 +67,7 @@ class CustomObjectTestCases(ValidatorTest):
         custom_obj = copy.deepcopy(self.valid_custom_object)
         custom_obj['type'] = "x-corporation"
         custom_obj['id'] = "x-corporation--4527e5de-8572-446a-a57a-706f15467461"
-        results = validate_instance(custom_obj, self.options)
+        results = validate_parsed_json(custom_obj, self.options)
         self.assertEqual(results.is_valid, False)
 
         self.assertTrueWithOptions(custom_obj, enabled='custom-prefix-lax')
@@ -77,7 +77,7 @@ class CustomObjectTestCases(ValidatorTest):
         custom_obj = copy.deepcopy(self.valid_custom_object)
         custom_obj['type'] = "x-corp-oration"
         custom_obj['id'] = "x-corp-oration--4527e5de-8572-446a-a57a-706f15467461"
-        results = validate_instance(custom_obj, self.options)
+        results = validate_parsed_json(custom_obj, self.options)
         self.assertTrue(results.is_valid)
 
     def test_strict_types(self):
diff --git a/stix2validator/test/identity_tests.py b/stix2validator/test/identity_tests.py
index 2acacf5..9fd89c6 100644
--- a/stix2validator/test/identity_tests.py
+++ b/stix2validator/test/identity_tests.py
@@ -3,11 +3,11 @@ import json
 import unittest
 
 from . import ValidatorTest
-from .. import validate_instance, validate_string
+from .. import validate_parsed_json, validate_string
 from ..errors import JSONError
 
 
-VALID_IDENTITY = """
+VALID_IDENTITY = u"""
 {
   "type": "identity",
   "id": "identity--2d1c6ab3-5e4e-48ac-a32b-f0c01c2836a8",
@@ -33,13 +33,13 @@ class IdentityTestCases(ValidatorTest):
     def test_vocab_identity_class(self):
         identity = copy.deepcopy(self.valid_identity)
         identity['identity_class'] = "corporation"
-        results = validate_instance(identity, self.options)
+        results = validate_parsed_json(identity, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_vocab_industry_sector(self):
         identity = copy.deepcopy(self.valid_identity)
         identity['sectors'] = ["something"]
-        results = validate_instance(identity, self.options)
+        results = validate_parsed_json(identity, self.options)
         self.assertEqual(results.is_valid, False)
 
         self.check_ignore(identity, 'industry-sector')
diff --git a/stix2validator/test/indicator_tests.py b/stix2validator/test/indicator_tests.py
index ebdf433..45b5f88 100644
--- a/stix2validator/test/indicator_tests.py
+++ b/stix2validator/test/indicator_tests.py
@@ -3,10 +3,10 @@ import json
 import unittest
 
 from . import ValidatorTest
-from .. import validate_instance, validate_string
+from .. import validate_parsed_json, validate_string
 
 
-VALID_INDICATOR = """
+VALID_INDICATOR = u"""
 {
     "type": "indicator",
     "id": "indicator--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
@@ -32,92 +32,92 @@ class IndicatorTestCases(ValidatorTest):
     def test_modified_before_created(self):
         indicator = copy.deepcopy(self.valid_indicator)
         indicator['modified'] = "2001-04-06T20:03:48Z"
-        results = validate_instance(indicator, self.options)
+        results = validate_parsed_json(indicator, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_custom_property_name_invalid_character(self):
         indicator = copy.deepcopy(self.valid_indicator)
         indicator['my_new_property!'] = "abc123"
-        results = validate_instance(indicator, self.options)
+        results = validate_parsed_json(indicator, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_custom_property_name_short(self):
         indicator = copy.deepcopy(self.valid_indicator)
         indicator['mp'] = "abc123"
-        results = validate_instance(indicator, self.options)
+        results = validate_parsed_json(indicator, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_custom_property_name_long(self):
         indicator = copy.deepcopy(self.valid_indicator)
         long_property_name = 'my_new_property_' * 16
         indicator[long_property_name] = "abc123"
-        results = validate_instance(indicator, self.options)
+        results = validate_parsed_json(indicator, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_empty_list(self):
         indicator = copy.deepcopy(self.valid_indicator)
         indicator['my_new_property'] = []
-        results = validate_instance(indicator, self.options)
+        results = validate_parsed_json(indicator, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_id_type(self):
         indicator = copy.deepcopy(self.valid_indicator)
         indicator['id'] = "something--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f"
-        results = validate_instance(indicator, self.options)
+        results = validate_parsed_json(indicator, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_reserved_property_confidence(self):
         indicator = copy.deepcopy(self.valid_indicator)
         indicator['confidence'] = "Something"
-        results = validate_instance(indicator, self.options)
+        results = validate_parsed_json(indicator, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_reserved_property_severity(self):
         indicator = copy.deepcopy(self.valid_indicator)
         indicator['severity'] = "Something"
-        results = validate_instance(indicator, self.options)
+        results = validate_parsed_json(indicator, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_reserved_property_action(self):
         indicator = copy.deepcopy(self.valid_indicator)
         indicator['action'] = "Something"
-        results = validate_instance(indicator, self.options)
+        results = validate_parsed_json(indicator, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_reserved_property_usernames(self):
         indicator = copy.deepcopy(self.valid_indicator)
         indicator['usernames'] = "Something"
-        results = validate_instance(indicator, self.options)
+        results = validate_parsed_json(indicator, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_reserved_property_phone_numbers(self):
         indicator = copy.deepcopy(self.valid_indicator)
         indicator['phone_numbers'] = "Something"
-        results = validate_instance(indicator, self.options)
+        results = validate_parsed_json(indicator, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_reserved_property_addresses(self):
         indicator = copy.deepcopy(self.valid_indicator)
         indicator['addresses'] = "Something"
-        results = validate_instance(indicator, self.options)
+        results = validate_parsed_json(indicator, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_reserved_object_type_incident(self):
         indicator = copy.deepcopy(self.valid_indicator)
         indicator['type'] = "incident"
-        results = validate_instance(indicator, self.options)
+        results = validate_parsed_json(indicator, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_reserved_object_type_infrastructure(self):
         indicator = copy.deepcopy(self.valid_indicator)
         indicator['type'] = "infrastructure"
-        results = validate_instance(indicator, self.options)
+        results = validate_parsed_json(indicator, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_vocab_indicator_label(self):
         indicator = copy.deepcopy(self.valid_indicator)
         indicator['labels'] = ["suspicious"]
-        results = validate_instance(indicator, self.options)
+        results = validate_parsed_json(indicator, self.options)
         self.assertEqual(results.is_valid, False)
 
         self.check_ignore(indicator, 'indicator-label')
diff --git a/stix2validator/test/intrusion_set_tests.py b/stix2validator/test/intrusion_set_tests.py
index 68d0516..18430c0 100644
--- a/stix2validator/test/intrusion_set_tests.py
+++ b/stix2validator/test/intrusion_set_tests.py
@@ -3,10 +3,10 @@ import json
 import unittest
 
 from . import ValidatorTest
-from .. import validate_instance, validate_string
+from .. import validate_parsed_json, validate_string
 
 
-VALID_INTRUSION_SET = """
+VALID_INTRUSION_SET = u"""
 {
   "type": "intrusion-set",
   "id": "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29",
@@ -31,13 +31,13 @@ class IntrusionSetTestCases(ValidatorTest):
     def test_country(self):
         intrusion_set = copy.deepcopy(self.valid_intrusion_set)
         intrusion_set['country'] = "USA"
-        results = validate_instance(intrusion_set, self.options)
+        results = validate_parsed_json(intrusion_set, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_vocab_attack_motivation(self):
         intrusion_set = copy.deepcopy(self.valid_intrusion_set)
         intrusion_set['primary_motivation'] = "selfishness"
-        results = validate_instance(intrusion_set, self.options)
+        results = validate_parsed_json(intrusion_set, self.options)
         self.assertEqual(results.is_valid, False)
 
         self.check_ignore(intrusion_set, 'attack-motivation')
@@ -45,7 +45,7 @@ class IntrusionSetTestCases(ValidatorTest):
     def test_vocab_attack_resource_level(self):
         intrusion_set = copy.deepcopy(self.valid_intrusion_set)
         intrusion_set['resource_level'] = "high"
-        results = validate_instance(intrusion_set, self.options)
+        results = validate_parsed_json(intrusion_set, self.options)
         self.assertEqual(results.is_valid, False)
 
         self.check_ignore(intrusion_set, 'attack-resource-level')
diff --git a/stix2validator/test/malware_tests.py b/stix2validator/test/malware_tests.py
index afbeeea..21070e5 100644
--- a/stix2validator/test/malware_tests.py
+++ b/stix2validator/test/malware_tests.py
@@ -3,10 +3,10 @@ import json
 import unittest
 
 from . import ValidatorTest
-from .. import validate_instance, validate_string
+from .. import validate_parsed_json, validate_string
 
 
-VALID_MALWARE = """
+VALID_MALWARE = u"""
 {
   "type": "malware",
   "id": "malware--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061",
@@ -29,7 +29,7 @@ class MalwareTestCases(ValidatorTest):
     def test_vocab_malware_label(self):
         malware = copy.deepcopy(self.valid_malware)
         malware['labels'] += "something"
-        results = validate_instance(malware, self.options)
+        results = validate_parsed_json(malware, self.options)
         self.assertEqual(results.is_valid, False)
 
         self.check_ignore(malware, 'malware-label')
diff --git a/stix2validator/test/marking_definition_tests.py b/stix2validator/test/marking_definition_tests.py
index 15e3cbe..20f36d1 100644
--- a/stix2validator/test/marking_definition_tests.py
+++ b/stix2validator/test/marking_definition_tests.py
@@ -3,10 +3,10 @@ import json
 import unittest
 
 from . import ValidatorTest
-from .. import validate_instance, validate_string
+from .. import validate_parsed_json, validate_string
 
 
-VALID_MARKING_DEFINITION = """
+VALID_MARKING_DEFINITION = u"""
 {
   "type": "marking-definition",
   "id": "marking-definition--34098fce-860f-48ae-8e50-ebd3cc5e41da",
@@ -29,7 +29,7 @@ class MarkingDefinitionTestCases(ValidatorTest):
     def test_vocab_marking_definition_label(self):
         marking_definition = copy.deepcopy(self.valid_marking_definition)
         marking_definition['definition_type'] = "something"
-        results = validate_instance(marking_definition, self.options)
+        results = validate_parsed_json(marking_definition, self.options)
         self.assertEqual(results.is_valid, False)
 
         self.check_ignore(marking_definition, 'marking-definition-type')
diff --git a/stix2validator/test/multi_obj_test.py b/stix2validator/test/multi_obj_test.py
new file mode 100644
index 0000000..bee5130
--- /dev/null
+++ b/stix2validator/test/multi_obj_test.py
@@ -0,0 +1,52 @@
+import copy
+import json
+
+from . import ValidatorTest
+from .. import validate_parsed_json, validate_string
+
+
+MULTI_OBJ_JSON = u"""
+[
+    {
+      "type": "attack-pattern",
+      "id": "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061",
+      "created": "2016-02-12T08:17:27.000Z",
+      "modified": "2016-02-12T08:17:27.000Z",
+      "name": "Spear Phishing",
+      "description": "...",
+      "external_references": [
+        {
+          "source_name": "capec",
+          "external_id": "CAPEC-463"
+        }
+      ]
+    },
+
+    {
+      "type": "identity",
+      "id": "identity--2d1c6ab3-5e4e-48ac-a32b-f0c01c2836a8",
+      "created": "2014-08-08T15:50:10.983Z",
+      "modified": "2014-08-08T15:50:10.983Z",
+      "name": "ACME Widget, Inc.",
+      "identity_class": "organization"
+    }
+]
+"""
+
+
+class MultiObjTestCases(ValidatorTest):
+    valid_objs = json.loads(MULTI_OBJ_JSON)
+
+    def test_both_valid(self):
+        obj_results = validate_string(MULTI_OBJ_JSON)
+        self.assertEqual(len(obj_results), 2)
+        self.assertTrue(all(r.is_valid for r in obj_results))
+
+    def test_one_valid_one_invalid(self):
+        objs = copy.deepcopy(self.valid_objs)
+        objs[1]["id"] = objs[1]["id"].replace("identity", "abc")
+        obj_results = validate_parsed_json(objs)
+
+        self.assertEqual(len(obj_results), 2)
+        self.assertTrue(obj_results[0].is_valid)
+        self.assertFalse(obj_results[1].is_valid)
diff --git a/stix2validator/test/network_traffic_tests.py b/stix2validator/test/network_traffic_tests.py
index 8fd46f5..8c99e6d 100644
--- a/stix2validator/test/network_traffic_tests.py
+++ b/stix2validator/test/network_traffic_tests.py
@@ -6,7 +6,7 @@ from . import ValidatorTest
 from .. import validate_string
 
 
-VALID_NETWORK_TRAFFIC_DEFINITION = """
+VALID_NETWORK_TRAFFIC_DEFINITION = u"""
 {
   "type": "observed-data",
   "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
diff --git a/stix2validator/test/observed_data_tests.py b/stix2validator/test/observed_data_tests.py
index 8145e66..c00084a 100644
--- a/stix2validator/test/observed_data_tests.py
+++ b/stix2validator/test/observed_data_tests.py
@@ -3,10 +3,10 @@ import json
 import unittest
 
 from . import ValidatorTest
-from .. import validate_instance, validate_string
+from .. import validate_parsed_json, validate_string
 
 
-VALID_OBSERVED_DATA_DEFINITION = """
+VALID_OBSERVED_DATA_DEFINITION = u"""
 {
   "type": "observed-data",
   "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
@@ -96,7 +96,7 @@ class ObservedDataTestCases(ValidatorTest):
           "objects.0.addons",
           "objects.9"
         ]
-        results = validate_instance(observed_data, self.options)
+        results = validate_parsed_json(observed_data, self.options)
         self.assertTrue(len(results.errors) == 3)
         self.assertFalse(results.is_valid)
 
@@ -107,7 +107,7 @@ class ObservedDataTestCases(ValidatorTest):
                 "foo": "bar"
             }
         }
-        results = validate_instance(observed_data, self.options)
+        results = validate_parsed_json(observed_data, self.options)
         self.assertTrue(len(results.errors) == 1)
         self.assertFalse(results.is_valid)
 
@@ -120,7 +120,7 @@ class ObservedDataTestCases(ValidatorTest):
                 "foo": "bar"
             }
         }
-        results = validate_instance(observed_data, self.options)
+        results = validate_parsed_json(observed_data, self.options)
         self.assertTrue(len(results.errors) == 1)
         self.assertFalse(results.is_valid)
 
@@ -255,7 +255,7 @@ class ObservedDataTestCases(ValidatorTest):
                 }
             }
         }
-        results = validate_instance(observed_data, self.options)
+        results = validate_parsed_json(observed_data, self.options)
         self.assertTrue(len(results.errors) == 2)
         self.assertFalse(results.is_valid)
 
diff --git a/stix2validator/test/relationship_tests.py b/stix2validator/test/relationship_tests.py
index ebe2372..e088283 100644
--- a/stix2validator/test/relationship_tests.py
+++ b/stix2validator/test/relationship_tests.py
@@ -3,10 +3,10 @@ import json
 import unittest
 
 from . import ValidatorTest
-from .. import validate_instance, validate_string
+from .. import validate_parsed_json, validate_string
 
 
-VALID_RELATIONSHIP = """
+VALID_RELATIONSHIP = u"""
 {
     "type": "relationship",
     "id": "relationship--44298a74-ba52-4f0c-87a3-1824e67d7fad",
@@ -30,33 +30,33 @@ class RelationshipTestCases(ValidatorTest):
     def test_relationship_type(self):
         relationship = copy.deepcopy(self.valid_relationship)
         relationship['relationship_type'] = "SOMETHING"
-        results = validate_instance(relationship, self.options)
+        results = validate_parsed_json(relationship, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_source_relationship(self):
         relationship = copy.deepcopy(self.valid_relationship)
         relationship['source_ref'] = "relationship--31b940d4-6f7f-459a-80ea-9c1f17b5891b"
-        results = validate_instance(relationship, self.options)
+        results = validate_parsed_json(relationship, self.options)
         self.assertEqual(results.is_valid, False)
         self.assertEqual(len(results.errors), 1)
 
     def test_source_sighting(self):
         relationship = copy.deepcopy(self.valid_relationship)
         relationship['source_ref'] = "sighting--31b940d4-6f7f-459a-80ea-9c1f17b5891b"
-        results = validate_instance(relationship, self.options)
+        results = validate_parsed_json(relationship, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_target_bundle(self):
         relationship = copy.deepcopy(self.valid_relationship)
         relationship['target_ref'] = "bundle--31b940d4-6f7f-459a-80ea-9c1f17b5891b"
-        results = validate_instance(relationship, self.options)
+        results = validate_parsed_json(relationship, self.options)
         self.assertEqual(results.is_valid, False)
         self.assertEqual(len(results.errors), 1)
 
     def test_target_marking_definition(self):
         relationship = copy.deepcopy(self.valid_relationship)
         relationship['target_ref'] = "marking-definition--31b940d4-6f7f-459a-80ea-9c1f17b5891b"
-        results = validate_instance(relationship, self.options)
+        results = validate_parsed_json(relationship, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_relationship_types_invalid_type(self):
@@ -64,7 +64,7 @@ class RelationshipTestCases(ValidatorTest):
         relationship['source_ref'] = "malware--31b940d4-6f7f-459a-80ea-9c1f17b5891b"
         relationship['target_ref'] = "campaign--9c1f891b-459a-6f7f-80ea-31b940d417b5"
         relationship['relationship_type'] = "mitigates"
-        results = validate_instance(relationship, self.options)
+        results = validate_parsed_json(relationship, self.options)
         self.assertEqual(results.is_valid, False)
 
         self.check_ignore(relationship, 'relationship-types')
@@ -84,7 +84,7 @@ class RelationshipTestCases(ValidatorTest):
         relationship['source_ref'] = "tool--31b940d4-6f7f-459a-80ea-9c1f17b5891b"
         relationship['target_ref'] = "vulnerability--9c1f891b-459a-6f7f-80ea-31b17b5940d4"
         relationship['relationship_type'] = "targets"
-        results = validate_instance(relationship, self.options)
+        results = validate_parsed_json(relationship, self.options)
         self.assertTrue(results.is_valid)
 
     def test_relationship_types_common(self):
@@ -92,7 +92,7 @@ class RelationshipTestCases(ValidatorTest):
         relationship['source_ref'] = "malware--31b940d4-6f7f-459a-80ea-9c1f17b5891b"
         relationship['target_ref'] = "campaign--9c1f891b-459a-6f7f-80ea-31b940d417b5"
         relationship['relationship_type'] = "related-to"
-        results = validate_instance(relationship, self.options)
+        results = validate_parsed_json(relationship, self.options)
         self.assertTrue(results.is_valid)
 
     def test_missing_required(self):
diff --git a/stix2validator/test/report_tests.py b/stix2validator/test/report_tests.py
index 57beff6..6c646da 100644
--- a/stix2validator/test/report_tests.py
+++ b/stix2validator/test/report_tests.py
@@ -3,10 +3,10 @@ import json
 import unittest
 
 from . import ValidatorTest
-from .. import validate_instance, validate_string
+from .. import validate_parsed_json, validate_string
 
 
-VALID_REPORT = """
+VALID_REPORT = u"""
 {
   "type": "report",
   "id": "report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3",
@@ -36,7 +36,7 @@ class IdentityTestCases(ValidatorTest):
     def test_vocab_report_label(self):
         report = copy.deepcopy(self.valid_report)
         report['labels'] = ["something"]
-        results = validate_instance(report, self.options)
+        results = validate_parsed_json(report, self.options)
         self.assertEqual(results.is_valid, False)
 
         self.check_ignore(report, 'report-label')
diff --git a/stix2validator/test/sighting_tests.py b/stix2validator/test/sighting_tests.py
index 906e39c..ad3423f 100644
--- a/stix2validator/test/sighting_tests.py
+++ b/stix2validator/test/sighting_tests.py
@@ -6,7 +6,7 @@ from . import ValidatorTest
 from .. import validate_string
 
 
-VALID_SIGHTING = """
+VALID_SIGHTING = u"""
 {
   "type": "sighting",
   "id": "sighting--6b0e3956-95f3-4c04-a882-116832996da0",
diff --git a/stix2validator/test/threat_actor_tests.py b/stix2validator/test/threat_actor_tests.py
index d2a5855..17ed4f8 100644
--- a/stix2validator/test/threat_actor_tests.py
+++ b/stix2validator/test/threat_actor_tests.py
@@ -3,10 +3,10 @@ import json
 import unittest
 
 from . import ValidatorTest
-from .. import validate_string
+from .. import validate_parsed_json, validate_string
 
 
-VALID_THREAT_ACTOR = """
+VALID_THREAT_ACTOR = u"""
 {
   "type": "threat-actor",
   "id": "threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
@@ -30,7 +30,7 @@ class ThreatActorTestCases(ValidatorTest):
     def test_vocab_attack_motivation(self):
         threat_actor = copy.deepcopy(self.valid_threat_actor)
         threat_actor['primary_motivation'] = "selfishness"
-        results = validate_string(json.dumps(threat_actor), self.options)
+        results = validate_parsed_json(threat_actor, self.options)
         self.assertEqual(results.is_valid, False)
 
         self.check_ignore(threat_actor, 'attack-motivation')
@@ -38,7 +38,7 @@ class ThreatActorTestCases(ValidatorTest):
     def test_vocab_attack_resource_level(self):
         threat_actor = copy.deepcopy(self.valid_threat_actor)
         threat_actor['resource_level'] = "high"
-        results = validate_string(json.dumps(threat_actor), self.options)
+        results = validate_parsed_json(threat_actor, self.options)
         self.assertEqual(results.is_valid, False)
 
         self.check_ignore(threat_actor, 'attack-resource-level')
@@ -46,7 +46,7 @@ class ThreatActorTestCases(ValidatorTest):
     def test_vocab_threat_actor_label(self):
         threat_actor = copy.deepcopy(self.valid_threat_actor)
         threat_actor['labels'] += ["anonymous"]
-        results = validate_string(json.dumps(threat_actor), self.options)
+        results = validate_parsed_json(threat_actor, self.options)
         self.assertEqual(results.is_valid, False)
 
         self.check_ignore(threat_actor, 'threat-actor-label')
@@ -54,7 +54,7 @@ class ThreatActorTestCases(ValidatorTest):
     def test_vocab_threat_actor_role(self):
         threat_actor = copy.deepcopy(self.valid_threat_actor)
         threat_actor['roles'] = ["contributor"]
-        results = validate_string(json.dumps(threat_actor), self.options)
+        results = validate_parsed_json(threat_actor, self.options)
         self.assertEqual(results.is_valid, False)
 
         self.check_ignore(threat_actor, 'threat-actor-role')
@@ -62,7 +62,7 @@ class ThreatActorTestCases(ValidatorTest):
     def test_vocab_threat_actor_sophistication_level(self):
         threat_actor = copy.deepcopy(self.valid_threat_actor)
         threat_actor['sophistication'] = "high"
-        results = validate_string(json.dumps(threat_actor), self.options)
+        results = validate_parsed_json(threat_actor, self.options)
         self.assertEqual(results.is_valid, False)
 
         self.check_ignore(threat_actor,
diff --git a/stix2validator/test/tool_tests.py b/stix2validator/test/tool_tests.py
index 30d1a7f..9ac7e49 100644
--- a/stix2validator/test/tool_tests.py
+++ b/stix2validator/test/tool_tests.py
@@ -3,10 +3,10 @@ import json
 import unittest
 
 from . import ValidatorTest
-from .. import validate_instance, validate_string
+from .. import validate_parsed_json, validate_string
 
 
-VALID_TOOL = """
+VALID_TOOL = u"""
 {
   "type": "tool",
   "id": "tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
@@ -35,7 +35,7 @@ class ToolTestCases(ValidatorTest):
     def test_vocab_tool_label(self):
         tool = copy.deepcopy(self.valid_tool)
         tool['labels'] += ["multi-purpose"]
-        results = validate_instance(tool, self.options)
+        results = validate_parsed_json(tool, self.options)
         self.assertEqual(results.is_valid, False)
 
         self.check_ignore(tool, 'tool-label')
@@ -43,15 +43,15 @@ class ToolTestCases(ValidatorTest):
     def test_kill_chain_name(self):
         tool = copy.deepcopy(self.valid_tool)
         tool['kill_chain_phases'][0]['kill_chain_name'] = "Something"
-        results = validate_instance(tool, self.options)
+        results = validate_parsed_json(tool, self.options)
         self.assertEqual(results.is_valid, False)
 
         tool['kill_chain_phases'][0]['kill_chain_name'] = "some thing"
-        results = validate_instance(tool, self.options)
+        results = validate_parsed_json(tool, self.options)
         self.assertEqual(results.is_valid, False)
 
         tool['kill_chain_phases'][0]['kill_chain_name'] = "some_thing"
-        results = validate_instance(tool, self.options)
+        results = validate_parsed_json(tool, self.options)
         self.assertEqual(results.is_valid, False)
 
         self.check_ignore(tool, 'kill-chain-names')
@@ -59,15 +59,15 @@ class ToolTestCases(ValidatorTest):
     def test_kill_chain_phase_name(self):
         tool = copy.deepcopy(self.valid_tool)
         tool['kill_chain_phases'][0]['phase_name'] = "Something"
-        results = validate_instance(tool, self.options)
+        results = validate_parsed_json(tool, self.options)
         self.assertEqual(results.is_valid, False)
 
         tool['kill_chain_phases'][0]['phase_name'] = "some thing"
-        results = validate_instance(tool, self.options)
+        results = validate_parsed_json(tool, self.options)
         self.assertEqual(results.is_valid, False)
 
         tool['kill_chain_phases'][0]['phase_name'] = "some_thing"
-        results = validate_instance(tool, self.options)
+        results = validate_parsed_json(tool, self.options)
         self.assertEqual(results.is_valid, False)
 
         self.check_ignore(tool, 'kill-chain-names')
diff --git a/stix2validator/test/vulnerability_tests.py b/stix2validator/test/vulnerability_tests.py
index 76dc761..074d677 100644
--- a/stix2validator/test/vulnerability_tests.py
+++ b/stix2validator/test/vulnerability_tests.py
@@ -3,10 +3,10 @@ import json
 import unittest
 
 from . import ValidatorTest
-from .. import validate_instance, validate_string
+from .. import validate_parsed_json, validate_string
 
 
-VALID_VULNERABILITY = """
+VALID_VULNERABILITY = u"""
 {
   "type": "vulnerability",
   "id": "vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061",
@@ -34,35 +34,35 @@ class VulnerabilityTestCases(ValidatorTest):
         vulnerability = copy.deepcopy(self.valid_vulnerability)
         ext_refs = vulnerability['external_references']
         ext_refs[0]['external_id'] = "2016-1234"
-        results = validate_instance(vulnerability, self.options)
+        results = validate_parsed_json(vulnerability, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_incorrect_cve_format_number(self):
         vulnerability = copy.deepcopy(self.valid_vulnerability)
         ext_refs = vulnerability['external_references']
         ext_refs[0]['external_id'] = "CVE-20161234"
-        results = validate_instance(vulnerability, self.options)
+        results = validate_parsed_json(vulnerability, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_incorrect_cve_format_letter(self):
         vulnerability = copy.deepcopy(self.valid_vulnerability)
         ext_refs = vulnerability['external_references']
         ext_refs[0]['external_id'] = "CVE-2016-abc"
-        results = validate_instance(vulnerability, self.options)
+        results = validate_parsed_json(vulnerability, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_incorrect_cve_format_lowercase(self):
         vulnerability = copy.deepcopy(self.valid_vulnerability)
         ext_refs = vulnerability['external_references']
         ext_refs[0]['external_id'] = "cve-2016-1234"
-        results = validate_instance(vulnerability, self.options)
+        results = validate_parsed_json(vulnerability, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_incorrect_cve_source_name(self):
         vulnerability = copy.deepcopy(self.valid_vulnerability)
         ext_refs = vulnerability['external_references']
         ext_refs[0]['source_name'] = "CVE"
-        results = validate_instance(vulnerability, self.options)
+        results = validate_parsed_json(vulnerability, self.options)
         self.assertEqual(results.is_valid, False)
 
     def test_url_no_hash(self):