Repository: omni-us/jsonargparse
Pull request #51 (2021-04-05 13:11:09)

Rewrite typehints


Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 183f3e5..6e7a47c 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -19,10 +19,17 @@ Added
 - Dataclasses are now supported as a type.
 - New predefined type Path_dc.
 - Support for Callable type.
+- Signature methods with nested key can be made required.
 
 Changed
 ^^^^^^^
 - Generation of yaml now uses internally pyyaml's safe_dump.
+- New cleaner implementation for type hints support.
+- Moved deprecated code to a module specific for this.
+
+Deprecated
+^^^^^^^^^^
+- ActionEnum should no longer be used, instead enums are given as type.
 
 
 v3.8.1 (2021-03-22)
diff --git a/README.rst b/README.rst
index 53fb464..83f4b6c 100644
--- a/README.rst
+++ b/README.rst
@@ -98,9 +98,7 @@ corresponding extras requires that enables them.
 +----------------------------------+------+-------------+---------+------------+------------+
 |                                  | urls | argcomplete | jsonnet | jsonschema | signatures |
 +----------------------------------+------+-------------+---------+------------+------------+
-| :ref:`boolean-arguments`         |      |             |         | ✓          | ✓          |
-+----------------------------------+------+-------------+---------+------------+------------+
-| :ref:`type-hints`                |      |             |         | ✓          | ✓          |
+| :ref:`type-hints`                |      |             |         |            | ✓          |
 +----------------------------------+------+-------------+---------+------------+------------+
 | :ref:`classes-methods-functions` |      |             |         |            | ✓          |
 +----------------------------------+------+-------------+---------+------------+------------+
@@ -108,7 +106,7 @@ corresponding extras requires that enables them.
 +----------------------------------+------+-------------+---------+------------+------------+
 | :ref:`parsing-urls`              | ✓    |             |         |            |            |
 +----------------------------------+------+-------------+---------+------------+------------+
-| :ref:`json-schemas`              |      |             |         | ✓          | ✓          |
+| :ref:`json-schemas`              |      |             |         | ✓          |            |
 +----------------------------------+------+-------------+---------+------------+------------+
 | :ref:`jsonnet-files`             |      |             | ✓       |            |            |
 +----------------------------------+------+-------------+---------+------------+------------+
@@ -523,13 +521,11 @@ This can be done by initializing :class:`.ArgumentParser` with
 :code:`logger={'level': 'DEBUG'}`. For more details about logging go to section
 :ref:`logging`.
 
-For all features described above to work, two optional packages are required:
-`jsonschema <https://pypi.org/project/jsonschema/>`__ to support validation of
-complex type hints and `docstring-parser
-<https://pypi.org/project/docstring-parser/>`__ to get the argument descriptions
-from the docstrings. Both these packages are included when jsonargparse is
-installed using the :code:`signatures` extras require as explained in section
-:ref:`installation`.
+For all features described above to work, one optional package is required:
+`docstring-parser <https://pypi.org/project/docstring-parser/>`__ to get the
+argument descriptions from the docstrings. This package is included when
+jsonargparse is installed using the :code:`signatures` extras require as
+explained in section :ref:`installation`.
 
 
 .. _type-hints:
@@ -1075,9 +1071,6 @@ is implemented. If given as values :code:`{'yes', 'true'}` or :code:`{'no',
     >>> parser.parse_args(['--op1', 'yes', '--op2', 'false'])
     Namespace(op1=True, op2=False)
 
-To use :code:`type=bool` jsonargparse needs to be installed with the
-:code:`jsonschema` extras require as explained in section :ref:`installation`.
-
 Sometimes it is also useful to define two paired options, one to set
 :code:`True` and the other to set :code:`False`. The :class:`.ActionYesNo` class
 makes this straightforward. A couple of examples would be:
diff --git a/jsonargparse/__init__.py b/jsonargparse/__init__.py
index 522ec82..d7f98fb 100644
--- a/jsonargparse/__init__.py
+++ b/jsonargparse/__init__.py
@@ -1,10 +1,12 @@
 from .actions import *
 from .cli import *
 from .core import *
+from .deprecated import *
 from .formatters import *
 from .jsonnet import *
 from .jsonschema import *
 from .optionals import *
+from .typehints import *
 from .util import *
 from argparse import (
     Action,
diff --git a/jsonargparse/actions.py b/jsonargparse/actions.py
index e8fd255..8caf2b1 100644
--- a/jsonargparse/actions.py
+++ b/jsonargparse/actions.py
@@ -7,10 +7,9 @@ import yaml
 import argparse
 from enum import Enum
 from typing import Type
-from argparse import Namespace, Action, SUPPRESS, _StoreAction, _HelpAction, _SubParsersAction
+from argparse import Namespace, Action, SUPPRESS, _HelpAction, _SubParsersAction
 
 from .optionals import get_config_read_mode, FilesCompleterMethod
-from .typing import restricted_number_type, registered_types
 from .util import (
     yamlParserError,
     yamlScannerError,
@@ -28,8 +27,6 @@ from .util import (
 __all__ = [
     'ActionConfigFile',
     'ActionYesNo',
-    'ActionEnum',
-    'ActionOperators',
     'ActionParser',
     'ActionPath',
     'ActionPathList',
@@ -299,88 +296,6 @@ class ActionYesNo(Action):
         return ['true', 'false', 'yes', 'no']
 
 
-class ActionEnum(Action):
-    """An action based on an Enum that maps to-from strings and enum values."""
-
-    def __init__(
-        self,
-        enum: Enum = None,
-        **kwargs
-    ):
-        """Initializer for ActionEnum instance.
-
-        Args:
-            enum: An Enum class.
-
-        Raises:
-            ValueError: If a parameter is invalid.
-        """
-        if enum is not None:
-            if not _issubclass(enum, Enum):
-                raise ValueError('Expected enum to be an subclass of Enum.')
-            self._enum = enum
-        elif '_enum' not in kwargs:
-            raise ValueError('Expected enum keyword argument.')
-        else:
-            self._enum = kwargs.pop('_enum')
-            kwargs['metavar'] = '{'+','.join(self._enum.__members__.keys())+'}'  # type: ignore
-            super().__init__(**kwargs)
-
-    def __call__(self, *args, **kwargs):
-        """Parses an argument mapping a string to its Enum value.
-
-        Raises:
-            TypeError: If value not present in the Enum.
-        """
-        if len(args) == 0:
-            kwargs['_enum'] = self._enum
-            return ActionEnum(**kwargs)
-        setattr(args[1], self.dest, self._check_type(args[2]))
-
-    def _check_type(self, value, cfg=None):
-        islist = _is_action_value_list(self)
-        if not islist:
-            value = [value]
-        for num, val in enumerate(value):
-            try:
-                if isinstance(val, str):
-                    value[num] = self._enum[val]
-                else:
-                    self._enum(val)
-            except KeyError as ex:
-                elem = '' if not islist else ' element '+str(num+1)
-                raise TypeError('Parser key "'+self.dest+'"'+elem+': value '+str(val)+' not in '+self._enum.__name__+'.') from ex
-        return value if islist else value[0]
-
-    def completer(self, **kwargs):
-        """Used by argcomplete to support tab completion of arguments."""
-        return list(self._enum.__members__.keys())
-
-
-class ActionOperators:
-    """DEPRECATED: Action to restrict a value with comparison operators.
-
-    The new alternative is explained in :ref:`restricted-numbers`.
-    """
-
-    def __init__(self, **kwargs):
-        if 'expr' in kwargs:
-            restrictions = [kwargs['expr']] if isinstance(kwargs['expr'], tuple) else kwargs['expr']
-            register_key = (tuple(sorted(restrictions)), kwargs.get('type', int), kwargs.get('join', 'and'))
-            if register_key in registered_types:
-                self._type = registered_types[register_key]
-            else:
-                self._type = restricted_number_type(None, kwargs.get('type', int), kwargs['expr'], kwargs.get('join', 'and'))
-        else:
-            raise ValueError('Expected expr keyword argument.')
-
-    def __call__(self, *args, **kwargs):
-        if 'type' in kwargs:
-            raise ValueError('ActionOperators does not allow type given to add_argument.')
-        kwargs['type'] = self._type
-        return _StoreAction(**kwargs)
-
-
 class ActionParser:
     """Action to parse option with a given parser optionally loading from file if string value."""
 
diff --git a/jsonargparse/core.py b/jsonargparse/core.py
index 3b39baf..86cfe37 100644
--- a/jsonargparse/core.py
+++ b/jsonargparse/core.py
@@ -7,7 +7,6 @@ import yaml
 import logging
 import inspect
 import argparse
-from enum import Enum
 from copy import deepcopy
 from contextlib import redirect_stderr
 from typing import Any, List, Dict, Set, Union, Optional, Type, Callable
@@ -15,8 +14,8 @@ from argparse import ArgumentError, Action, Namespace, SUPPRESS
 
 from .formatters import DefaultHelpFormatter
 from .signatures import SignatureArguments
-from .typing import type_in
-from .jsonschema import ActionJsonSchema, supported_types
+from .typehints import ActionTypeHint
+from .jsonschema import ActionJsonSchema
 from .jsonnet import ActionJsonnet
 from .optionals import (
     dump_preserve_order_support,
@@ -29,7 +28,6 @@ from .optionals import (
     TypeCastCompleterMethod,
 )
 from .actions import (
-    ActionEnum,
     ActionParser,
     ActionConfigFile,
     ActionPath,
@@ -105,15 +103,10 @@ class _ActionsContainer(SignatureArguments, argparse._ActionsContainer):
                 nested_key = re.sub('^--', '', args[0])
                 super().add_dataclass_arguments(theclass, nested_key, **kwargs)
                 return _find_action(parser, nested_key)
-            if type_in(kwargs['type'], supported_types) or \
-               (inspect.isclass(kwargs['type']) and not _issubclass(kwargs['type'], (str, int, float, Enum, Path))):
+            if ActionTypeHint.is_supported_typehint(kwargs['type']):
                 if 'action' in kwargs:
-                    raise ValueError('Type hints as type does not allow providing an action')
-                kwargs['action'] = ActionJsonSchema(annotation=kwargs.pop('type'), enable_path=enable_path)
-            elif _issubclass(kwargs['type'], Enum):
-                if 'action' in kwargs:
-                    raise ValueError('Enum as type does not allow providing an action')
-                kwargs['action'] = ActionEnum(enum=kwargs['type'])
+                    raise ValueError('Type hint as type does not allow providing an action')
+                kwargs['action'] = ActionTypeHint(typehint=kwargs.pop('type'), enable_path=enable_path)
         action = super().add_argument(*args, **kwargs)
         if not hasattr(action, 'completer') and action.type is not None:
             completer_method = FilesCompleterMethod if isinstance(action.type, Path) else TypeCastCompleterMethod
@@ -273,6 +266,7 @@ class ArgumentParser(_ActionsContainer, argparse.ArgumentParser, LoggerProperty)
         nested: bool,
         with_meta: Optional[bool],
         skip_check: bool,
+        skip_required: bool = False,
         skip_subcommands: bool = False,
         fail_no_subcommand: bool = True,
         cfg_base: Union[Namespace, Dict[str, Any]] = None,
@@ -324,7 +318,10 @@ class ArgumentParser(_ActionsContainer, argparse.ArgumentParser, LoggerProperty)
             self.exit()
 
         if not skip_check:
-            self.check_config(cfg_ns)
+            self.check_config(cfg_ns, skip_required=skip_required)
+
+        if not nested:
+            cfg_ns = _dict_to_flat_namespace(namespace_to_dict(cfg_ns))
 
         if log_message is not None:
             self._logger.info(log_message)
@@ -397,6 +394,7 @@ class ArgumentParser(_ActionsContainer, argparse.ArgumentParser, LoggerProperty)
         nested: bool = True,
         with_meta: bool = None,
         _skip_check: bool = False,
+        _skip_required: bool = False,
     ) -> Union[Namespace, Dict[str, Any]]:
         """Parses configuration given as an object.
 
@@ -416,6 +414,7 @@ class ArgumentParser(_ActionsContainer, argparse.ArgumentParser, LoggerProperty)
         try:
             cfg = vars(_dict_to_flat_namespace(cfg_obj))
             self._apply_actions(cfg, self._actions)
+            cfg = _flat_namespace_to_dict(dict_to_namespace(cfg))
 
             parsed_cfg = self._parse_common(
                 cfg=cfg,
@@ -424,6 +423,7 @@ class ArgumentParser(_ActionsContainer, argparse.ArgumentParser, LoggerProperty)
                 nested=nested,
                 with_meta=with_meta,
                 skip_check=_skip_check,
+                skip_required=_skip_required,
                 cfg_base=cfg_base,
                 log_message='Parsed object.',
             )
@@ -703,7 +703,7 @@ class ArgumentParser(_ActionsContainer, argparse.ArgumentParser, LoggerProperty)
             self.check_config(cfg)
 
         def cleanup_actions(cfg, actions):
-            for action in actions:
+            for action in filter_default_actions(actions):
                 if (action.help == SUPPRESS and not isinstance(action, _ActionConfigLoad)) or \
                    isinstance(action, ActionConfigFile) or \
                    (skip_none and action.dest in cfg and cfg[action.dest] is None):
@@ -714,26 +714,11 @@ class ArgumentParser(_ActionsContainer, argparse.ArgumentParser, LoggerProperty)
                             cfg[action.dest] = [str(p) for p in cfg[action.dest]]
                         else:
                             cfg[action.dest] = str(cfg[action.dest])
-                elif isinstance(action, ActionJsonSchema) and action._annotation is not None and cfg.get(action.dest) is not None:
-                    cfg[action.dest] = ActionJsonSchema._adapt_types(cfg[action.dest], action._annotation, action._subschemas, True)
-
-        def cleanup_types(cfg):
-            for dest, val in cfg.items():
-                if isinstance(val, Enum):
-                    cfg[dest] = val.name
-                elif isinstance(val, Path):
-                    cfg[dest] = str(val)
-                elif 'jsonargparse.typing.' in str(type(val)):
-                    if isinstance(val, str):
-                        cfg[dest] = str(val)
-                    elif isinstance(val, int):
-                        cfg[dest] = int(val)
-                    elif isinstance(val, float):
-                        cfg[dest] = float(val)
+                elif isinstance(action, ActionTypeHint) and cfg.get(action.dest) is not None:
+                    cfg[action.dest] = ActionTypeHint.serialize(cfg[action.dest], action._typehint)
 
         cfg = namespace_to_dict(_dict_to_flat_namespace(cfg))
         cleanup_actions(cfg, self._actions)
-        cleanup_types(cfg)
         cfg = _flat_namespace_to_dict(_dict_to_flat_namespace(cfg))
 
         if format == 'parser_mode':
@@ -891,7 +876,7 @@ class ArgumentParser(_ActionsContainer, argparse.ArgumentParser, LoggerProperty)
             An object with all default values as attributes.
         """
         cfg = {}
-        for action in self._actions:
+        for action in filter_default_actions(self._actions):
             if action.default != SUPPRESS and action.dest != SUPPRESS:
                 cfg[action.dest] = action.default
 
@@ -903,17 +888,22 @@ class ArgumentParser(_ActionsContainer, argparse.ArgumentParser, LoggerProperty)
         if default_config_file is not None:
             with change_to_path_dir(default_config_file):
                 cfg_file = self._load_cfg(default_config_file.get_content())
-                if not skip_check:
-                    try:
-                        self.check_config(cfg_file, skip_required=True)
-                    except (TypeError, KeyError) as ex:
-                        raise ParserError('Problem in default config file "'+str(default_config_file)+'" :: '+ex.args[0]) from ex
+                try:
+                    cfg_file = self.parse_object(  # type: ignore
+                        _flat_namespace_to_dict(dict_to_namespace(cfg_file)),
+                        defaults=False,
+                        nested=False,
+                        _skip_check=skip_check,
+                        _skip_required=True,
+                    )
+                except (TypeError, KeyError, ParserError) as ex:
+                    raise ParserError('Problem in default config file "'+str(default_config_file)+'" :: '+ex.args[0]) from ex
             cfg = self._merge_config(cfg_file, cfg)
             cfg['__default_config__'] = default_config_file
             self._logger.info('Parsed configuration from default path: %s', str(default_config_file))
 
         if nested:
-            cfg = _flat_namespace_to_dict(Namespace(**cfg))
+            cfg = _flat_namespace_to_dict(dict_to_namespace(cfg))
 
         return dict_to_namespace(cfg)
 
@@ -1009,17 +999,18 @@ class ArgumentParser(_ActionsContainer, argparse.ArgumentParser, LoggerProperty)
             A configuration object with all subclasses instantiated.
         """
         cfg = namespace_to_dict(strip_meta(cfg))
-        actions = list(self._actions)
+        actions = filter_default_actions(self._actions)
         actions.sort(key=lambda x: -len(x.dest.split('.')))
         for action in actions:
-            if isinstance(action, ActionJsonSchema) or \
+            if isinstance(action, ActionTypeHint) or \
                (isinstance(action, _ActionConfigLoad) and is_pure_dataclass(action.basetype)):
                 try:
                     value, parent, key = _get_key_value(cfg, action.dest, parent=True)
                 except KeyError:
                     pass
                 else:
-                    parent[key] = action._instantiate_classes(value)
+                    if value is not None:
+                        parent[key] = action._instantiate_classes(value)
         return cfg if self._parse_as_dict else dict_to_namespace(cfg)
 
 
@@ -1066,7 +1057,7 @@ class ArgumentParser(_ActionsContainer, argparse.ArgumentParser, LoggerProperty)
         cfg_files = []
         if '__default_config__' in cfg:
             cfg_files.append(cfg['__default_config__'])
-        for action in self._actions:
+        for action in filter_default_actions(self._actions):
             if isinstance(action, ActionConfigFile) and action.dest in cfg and cfg[action.dest] is not None:
                 cfg_files.extend(p for p in cfg[action.dest] if p is not None)
         return cfg_files
@@ -1078,7 +1069,7 @@ class ArgumentParser(_ActionsContainer, argparse.ArgumentParser, LoggerProperty)
             note = 'no existing default config file found.'
             if '__default_config__' in defaults:
                 note = 'default values below will be ones overridden by the contents of: '+str(defaults['__default_config__'])
-                self.formatter_class.defaults = vars(_dict_to_flat_namespace(defaults))
+                self.formatter_class.defaults = defaults
             group = self._default_config_files_group
             group.description = str(self._default_config_files) + ', Note: '+note
         help_str = super().format_help()
@@ -1089,10 +1080,32 @@ class ArgumentParser(_ActionsContainer, argparse.ArgumentParser, LoggerProperty)
 
     def _apply_actions(self, cfg, actions):
         """Runs _check_value_key on actions present in flat config dict."""
-        for action in actions:
-            if action.dest in cfg:
+        def find_parent_action(key):
+            parts = key.split('.')
+            for n in reversed(range(len(parts)-1)):
+                action = _find_action(Namespace(_actions=actions), '.'.join(parts[:n+1]))
+                if action is not None:
+                    return action
+
+        keys = list(cfg.keys())
+        keys.sort(key=lambda x: -len(x.split('.')))
+        seen_keys = set()
+        for key in keys:
+            if key in seen_keys:
+                continue
+            action = _find_action(self, key)
+            if action is not None:
                 value = self._check_value_key(action, cfg[action.dest], action.dest, cfg)
                 cfg[action.dest] = value
+                continue
+            action = find_parent_action(key)
+            if action is not None:
+                value = {k[len(action.dest)+1:]: v for k, v in cfg.items() if k.startswith(action.dest+'.')}
+                value = _flat_namespace_to_dict(Namespace(**value))
+                value = self._check_value_key(action, value, action.dest, cfg)
+                value = vars(_dict_to_flat_namespace(value))
+                cfg.update({action.dest+'.'+k: v for k, v in value.items()})
+                seen_keys.update(action.dest+'.'+k for k in value.keys())
 
 
     @staticmethod
diff --git a/jsonargparse/deprecated.py b/jsonargparse/deprecated.py
new file mode 100644
index 0000000..80e36d8
--- /dev/null
+++ b/jsonargparse/deprecated.py
@@ -0,0 +1,55 @@
+"""Deprecated code."""
+
+from enum import Enum
+from .typehints import ActionTypeHint
+from .typing import restricted_number_type, registered_types
+from .util import _issubclass
+
+
+__all__ = [
+    'ActionEnum',
+    'ActionOperators',
+]
+
+
+class ActionEnum:
+    """DEPRECATED: An action based on an Enum that maps to-from strings and enum values.
+
+    Enums now are intended to be given directly as a type.
+    """
+
+    def __init__(self, **kwargs):
+        if 'enum' in kwargs:
+            if not _issubclass(kwargs['enum'], Enum):
+                raise ValueError('Expected enum to be an subclass of Enum.')
+            self._type = kwargs['enum']
+        else:
+            raise ValueError('Expected enum keyword argument.')
+
+    def __call__(self, *args, **kwargs):
+        if 'type' in kwargs:
+            raise ValueError('ActionEnum does not allow type given to add_argument.')
+        return ActionTypeHint(typehint=self._type)(**kwargs)
+
+
+class ActionOperators:
+    """DEPRECATED: Action to restrict a value with comparison operators.
+
+    The new alternative is explained in :ref:`restricted-numbers`.
+    """
+
+    def __init__(self, **kwargs):
+        if 'expr' in kwargs:
+            restrictions = [kwargs['expr']] if isinstance(kwargs['expr'], tuple) else kwargs['expr']
+            register_key = (tuple(sorted(restrictions)), kwargs.get('type', int), kwargs.get('join', 'and'))
+            if register_key in registered_types:
+                self._type = registered_types[register_key]
+            else:
+                self._type = restricted_number_type(None, kwargs.get('type', int), kwargs['expr'], kwargs.get('join', 'and'))
+        else:
+            raise ValueError('Expected expr keyword argument.')
+
+    def __call__(self, *args, **kwargs):
+        if 'type' in kwargs:
+            raise ValueError('ActionOperators does not allow type given to add_argument.')
+        return ActionTypeHint(typehint=self._type)(**kwargs)
diff --git a/jsonargparse/formatters.py b/jsonargparse/formatters.py
index 752fdb6..1fa0fa8 100644
--- a/jsonargparse/formatters.py
+++ b/jsonargparse/formatters.py
@@ -1,12 +1,11 @@
 """Formatter classes."""
 
+from argparse import _HelpAction, HelpFormatter, OPTIONAL, SUPPRESS, ZERO_OR_MORE
 from enum import Enum
-from argparse import HelpFormatter, _HelpAction, OPTIONAL, SUPPRESS, ZERO_OR_MORE
 
-from .util import _get_env_var
-from .typing import type_to_str
-from .actions import ActionEnum, ActionYesNo, ActionConfigFile
-from .jsonschema import ActionJsonSchema
+from .actions import ActionYesNo, ActionConfigFile
+from .typehints import ActionTypeHint, type_to_str
+from .util import _get_env_var, _get_key_value
 
 
 __all__ = ['DefaultHelpFormatter']
@@ -73,7 +72,7 @@ class DefaultHelpFormatter(HelpFormatter):
             params['type'] = type_str
         if 'default' in params:
             if hasattr(self, 'defaults'):
-                params['default'] = self.defaults[action.dest]
+                params['default'] = _get_key_value(self.defaults, action.dest)
             if params['default'] is None:
                 params['default'] = 'null'
             elif isinstance(params['default'], Enum) and hasattr(params['default'], 'name'):
@@ -87,8 +86,6 @@ class DefaultHelpFormatter(HelpFormatter):
             type_str = 'bool'
         elif action.type is not None:
             type_str = type_to_str(action.type)
-        elif isinstance(action, ActionEnum):
-            type_str = type_to_str(action._enum)
-        elif isinstance(action, ActionJsonSchema):
-            type_str = type_to_str(action._annotation)
+        elif isinstance(action, ActionTypeHint):
+            type_str = type_to_str(action._typehint)
         return type_str
diff --git a/jsonargparse/jsonschema.py b/jsonargparse/jsonschema.py
index a7ee60e..329ef81 100644
--- a/jsonargparse/jsonschema.py
+++ b/jsonargparse/jsonschema.py
@@ -1,65 +1,35 @@
-"""Action to support jsonschema and type hint annotations."""
+"""Action to support jsonschemas."""
 
-import os
 import json
+import os
 import yaml
-import inspect
-from enum import Enum
-from argparse import Namespace, Action
-from typing import Any, Callable, Dict, Iterable, List, Sequence, Set, Tuple, Type, Union
+from argparse import Action, Namespace
+from typing import Dict, Union
 
 from .actions import _is_action_value_list
-from .typing import is_optional, type_to_str, registered_types
+from .optionals import (
+    argcomplete_warn_redraw_prompt,
+    import_jsonschema,
+    jsonschemaValidationError,
+)
 from .util import (
     namespace_to_dict,
-    Path,
+    strip_meta,
     yamlParserError,
     yamlScannerError,
-    ParserError,
-    strip_meta,
-    import_object,
     _load_config,
-    _issubclass,
-)
-from .optionals import (
-    ModuleNotFound,
-    jsonschemaValidationError,
-    import_jsonschema,
-    files_completer,
-    argcomplete_warn_redraw_prompt,
 )
 
 
 __all__ = ['ActionJsonSchema']
 
 
-typesmap = {
-    str: 'string',
-    int: 'integer',
-    float: 'number',
-    bool: 'boolean',
-    type(None): 'null',
-}
-
-supported_types = {
-    bool,
-    Any,
-    Union,
-    List, list, Iterable, Sequence,
-    Tuple, tuple,
-    Set, set,
-    Dict, dict,
-    Callable,
-}
-
-
 class ActionJsonSchema(Action):
     """Action to parse option as json validated by a jsonschema."""
 
     def __init__(
         self,
         schema: Union[str, Dict] = None,
-        annotation: Type = None,
         enable_path: bool = True,
         with_meta: bool = True,
         **kwargs
@@ -68,7 +38,6 @@ class ActionJsonSchema(Action):
 
         Args:
             schema: Schema to validate values against.
-            annotation: Type object from which to generate schema.
             enable_path: Whether to try to load json from path (def.=True).
             with_meta: Whether to include metadata (def.=True).
 
@@ -76,17 +45,7 @@ class ActionJsonSchema(Action):
             ValueError: If a parameter is invalid.
             jsonschema.exceptions.SchemaError: If the schema is invalid.
         """
-        if schema is not None or annotation is not None:
-            if annotation is not None:
-                if schema is not None:
-                    raise ValueError('Only one of schema or annotation is accepted.')
-                self._annotation = annotation
-                schema, subschemas = ActionJsonSchema._typing_schema(self._annotation)
-                if schema is None or schema == {'type': 'null'}:
-                    raise ValueError('Unable to generate schema from annotation '+str(self._annotation))
-                self._subschemas = subschemas
-            else:
-                self._annotation = self._subschemas = None  # type: ignore
+        if schema is not None:
             if isinstance(schema, str):
                 try:
                     schema = yaml.safe_load(schema)
@@ -98,16 +57,11 @@ class ActionJsonSchema(Action):
             self._enable_path = enable_path
             self._with_meta = with_meta
         elif '_validator' not in kwargs:
-            raise ValueError('Expected schema or annotation keyword arguments.')
+            raise ValueError('Expected schema keyword argument.')
         else:
-            self._annotation = kwargs.pop('_annotation')
-            self._subschemas = kwargs.pop('_subschemas')
             self._validator = kwargs.pop('_validator')
             self._enable_path = kwargs.pop('_enable_path')
             self._with_meta = kwargs.pop('_with_meta')
-            metavar = self._annotation_metavar()
-            if metavar is not None:
-                kwargs['metavar'] = metavar
             super().__init__(**kwargs)
 
 
@@ -118,8 +72,6 @@ class ActionJsonSchema(Action):
             TypeError: If the argument is not valid.
         """
         if len(args) == 0:
-            kwargs['_annotation'] = self._annotation
-            kwargs['_subschemas'] = self._subschemas
             kwargs['_validator'] = self._validator
             kwargs['_enable_path'] = self._enable_path
             kwargs['_with_meta'] = self._with_meta
@@ -141,10 +93,8 @@ class ActionJsonSchema(Action):
                 val, fpath = _load_config(val, enable_path=self._enable_path, flat_namespace=False)
                 if isinstance(val, Namespace):
                     val = namespace_to_dict(val)
-                val = self._adapt_types(val, self._annotation, self._subschemas, reverse=True)
                 path_meta = val.pop('__path__') if isinstance(val, dict) and '__path__' in val else None
                 self._validator.validate(val)
-                val = self._adapt_types(val, self._annotation, self._subschemas)
                 if path_meta is not None:
                     val['__path__'] = path_meta
                 if isinstance(val, dict) and fpath is not None:
@@ -173,217 +123,9 @@ class ActionJsonSchema(Action):
         return jsonschema.validators.extend(validator_class, {'properties': set_defaults})
 
 
-    def _instantiate_classes(self, val):
-        if self._annotation is not None:
-            val = self._adapt_types(val, self._annotation, self._subschemas, instantiate_classes=True)
-        return val
-
-
-    @staticmethod
-    def _adapt_types(val, annotation, subschemas, reverse=False, instantiate_classes=False):
-
-        def validate_adapt(v, subschema, annotation=None):
-            if subschema is not None:
-                if isinstance(subschema, list):
-                    vals = []
-                    for s in subschema:
-                        try:
-                            vv = validate_adapt(v, s)
-                            if reverse and s[0] in typesmap and not isinstance(vv, s[0]):
-                                raise ValueError('Value "'+str(vv)+'" not instance of '+str(s[0]))
-                            vals.append(vv)
-                            break
-                        except Exception as ex:
-                            vals.append(ex)
-                    if all(isinstance(v, Exception) for v in vals):
-                        e = ' :: '.join(str(v) for v in vals)
-                        raise ParserError('Value "'+str(val)+'" does not validate against any of the types in '+str(annotation)+' :: '+e)
-                    return [v for v in vals if not isinstance(v, Exception)][0]
-
-                subannotation, subvalidator, subsubschemas = subschema
-                if reverse:
-                    v = ActionJsonSchema._adapt_types(v, subannotation, subsubschemas, reverse, instantiate_classes)
-                else:
-                    try:
-                        if subvalidator is not None and not instantiate_classes:
-                            subvalidator.validate(v)
-                        v = ActionJsonSchema._adapt_types(v, subannotation, subsubschemas, reverse, instantiate_classes)
-                    except jsonschemaValidationError as ex:
-                        raise ValueError(str(ex)) from ex
-            return v
-
-        if subschemas is None:
-            subschemas = []
-
-        if annotation in registered_types:
-            registered_type = registered_types[annotation]
-            if reverse and registered_type.is_value_of_type(val):
-                val = registered_type.serializer(val)
-            elif not reverse:
-                val = registered_type.deserializer(val)
-
-        elif _issubclass(annotation, Enum):
-            if reverse and isinstance(val, annotation):
-                val = val.name
-            elif not reverse and val in annotation.__members__:
-                val = annotation[val]
-
-        elif not hasattr(annotation, '__origin__'):
-            if not reverse and \
-               not _issubclass(annotation, (str, int, float)) and \
-               isinstance(val, dict) and \
-               'class_path' in val:
-                try:
-                    val_class = import_object(val['class_path'])
-                    assert _issubclass(val_class, annotation), 'Not a subclass of '+annotation.__name__
-                    if 'init_args' in val:
-                        from jsonargparse import ArgumentParser
-                        parser = ArgumentParser(error_handler=None, parse_as_dict=True)
-                        parser.add_class_arguments(val_class)
-                        parser.check_config(val['init_args'])
-                        if instantiate_classes:
-                            init_args = parser.instantiate_subclasses(val['init_args'])
-                            val = val_class(**init_args)  # pylint: disable=not-a-mapping
-                    elif instantiate_classes:
-                        val = val_class()
-                except (ImportError, ModuleNotFound, AttributeError, AssertionError, ParserError) as ex:
-                    raise ParserError('Problem with given class_path "'+val['class_path']+'" :: '+str(ex)) from ex
-            return val
-
-        elif annotation.__origin__ == Union:
-            val = validate_adapt(val, subschemas, annotation)
-
-        elif annotation.__origin__ in {Tuple, tuple, Set, set} and isinstance(val, (list, tuple, set)):
-            if reverse:
-                val = list(val)
-            for n, v in enumerate(val):
-                subschema = subschemas[n if n < len(subschemas) else -1]
-                if subschema is not None:
-                    val[n] = validate_adapt(v, subschema, annotation)
-            if not reverse:
-                val = tuple(val) if annotation.__origin__ in {Tuple, tuple} else set(val)
-
-        elif annotation.__origin__ in {List, list, Set, set, Iterable, Sequence} and isinstance(val, list):
-            for n, v in enumerate(val):
-                val[n] = validate_adapt(v, subschemas, annotation)
-
-        elif annotation.__origin__ in {Dict, dict} and isinstance(val, dict):
-            if annotation.__args__[0] == int:
-                cast = str if reverse else int
-                val = {cast(k): v for k, v in val.items()}
-            if annotation.__args__[1] not in typesmap:
-                for k, v in val.items():
-                    val[k] = validate_adapt(v, subschemas, annotation)
-
-        return val
-
-
-    @staticmethod
-    def _typing_schema(annotation):
-        """Generates a schema based on a type annotation."""
-
-        jsonvalidator = import_jsonschema('ActionJsonSchema')[1]
-
-        if annotation == Any or annotation in registered_types:
-            return {}, None
-
-        elif annotation in typesmap:
-            return {'type': typesmap[annotation]}, None
-
-        elif _issubclass(annotation, Enum):
-            return {'type': 'string', 'enum': list(annotation.__members__.keys())}, [(annotation, None, None)]
-
-        elif annotation == dict:
-            return {'type': 'object'}, None
-
-        elif not hasattr(annotation, '__origin__'):
-            if annotation != inspect._empty:
-                schema = {
-                    'type': 'object',
-                    'properties': {
-                        'class_path': {'type': 'string'},
-                        'init_args': {'type': 'object'},
-                    },
-                    'required': ['class_path'],
-                    'additionalProperties': False,
-                }
-                return schema, [(annotation, jsonvalidator(schema), None)]
-            return None, None
-
-        elif annotation.__origin__ == Union:
-            members = []
-            union_subschemas = []
-            for arg in annotation.__args__:
-                schema, subschemas = ActionJsonSchema._typing_schema(arg)
-                if schema is not None:
-                    members.append(schema)
-                    union_subschemas.append((arg, jsonvalidator(schema), subschemas))
-            if len(members) == 1:
-                return members[0], union_subschemas
-            elif len(members) > 1:
-                return {'anyOf': members}, union_subschemas
-
-        elif annotation.__origin__ in {Tuple, tuple}:
-            has_ellipsis = False
-            items = []
-            tuple_subschemas = []
-            for arg in annotation.__args__:
-                if arg == Ellipsis:
-                    has_ellipsis = True
-                    break
-                item, subschemas = ActionJsonSchema._typing_schema(arg)
-                items.append(item)
-                tuple_subschemas.append((arg, jsonvalidator(item), subschemas))
-            schema = {'type': 'array', 'items': items, 'minItems': len(items)}
-            if has_ellipsis:
-                schema['additionalItems'] = items[-1]
-            else:
-                schema['maxItems'] = len(items)
-            return schema, tuple_subschemas if tuple_subschemas != [] else None
-
-        elif annotation.__origin__ in {List, list, Iterable, Sequence, Set, set}:
-            items, subschemas = ActionJsonSchema._typing_schema(annotation.__args__[0])
-            if items is not None:
-                if subschemas is None:
-                    subschemas = [(annotation.__args__[0], None, None)]
-                else:
-                    subschemas = [(annotation.__args__[0], jsonvalidator(items), subschemas)]
-                return {'type': 'array', 'items': items}, subschemas
-
-        elif annotation.__origin__ in {Dict, dict} and annotation.__args__[0] in {str, int}:
-            pattern = {str: '.*', int: '[0-9]+'}[annotation.__args__[0]]
-            schema, subschemas = ActionJsonSchema._typing_schema(annotation.__args__[1])
-            if schema is not None:
-                return {'type': 'object', 'patternProperties': {pattern: schema}}, subschemas
-
-        return None, None
-
-
-    def _annotation_metavar(self):
-        """Generates a metavar for some types."""
-        metavar = None
-        if self._annotation == bool:
-            metavar = '{true,false}'
-        elif is_optional(self._annotation, bool):
-            metavar = '{true,false,null}'
-        elif is_optional(self._annotation, Enum):
-            enum = self._annotation.__args__[0]
-            metavar = '{'+','.join(list(enum.__members__.keys())+['null'])+'}'
-        return metavar
-
-
     def completer(self, prefix, **kwargs):
         """Used by argcomplete, validates value and shows expected type."""
-        if self._annotation == bool:
-            return ['true', 'false']
-        elif is_optional(self._annotation, bool):
-            return ['true', 'false', 'null']
-        elif is_optional(self._annotation, Enum):
-            enum = self._annotation.__args__[0]
-            return list(enum.__members__.keys())+['null']
-        elif is_optional(self._annotation, Path):
-            return ['null'] + sorted(files_completer(prefix, **kwargs))
-        elif chr(int(os.environ['COMP_TYPE'])) == '?':
+        if chr(int(os.environ['COMP_TYPE'])) == '?':
             try:
                 if prefix.strip() == '':
                     raise ValueError()
@@ -391,8 +133,6 @@ class ActionJsonSchema(Action):
                 msg = 'value already valid, '
             except (ValueError, yamlParserError, yamlScannerError, jsonschemaValidationError):
                 msg = 'value not yet valid, '
-            if self._annotation is not None:
-                msg += 'expected type '+type_to_str(self._annotation)
             else:
                 schema = json.dumps(self._validator.schema, indent=2, sort_keys=True).replace('\n', '\n  ')
                 msg += 'required to be valid according to schema:\n  '+schema+'\n'
diff --git a/jsonargparse/optionals.py b/jsonargparse/optionals.py
index e95dde9..4ed0f39 100644
--- a/jsonargparse/optionals.py
+++ b/jsonargparse/optionals.py
@@ -23,7 +23,7 @@ _argcomplete = find_spec('argcomplete')
 _dataclasses = find_spec('dataclasses')
 
 jsonschema_support = False if _jsonschema is None else True
-jsonnet_support = False if any(x is None for x in [_jsonnet, _jsonschema]) else True
+jsonnet_support = False if _jsonnet is None else True
 url_support = False if any(x is None for x in [_url_validator, _requests]) else True
 docstring_parser_support = False if _docstring_parser is None else True
 argcomplete_support = False if _argcomplete is None else True
diff --git a/jsonargparse/signatures.py b/jsonargparse/signatures.py
index aec71ca..04b06de 100644
--- a/jsonargparse/signatures.py
+++ b/jsonargparse/signatures.py
@@ -2,13 +2,11 @@
 
 import re
 import inspect
-from enum import Enum
 from typing import Callable, Container, Optional, Type, Union
 
+from .actions import _ActionConfigLoad, _ActionHelpClassPath
+from .typehints import ActionTypeHint, is_optional
 from .util import _issubclass
-from .actions import ActionEnum, _ActionConfigLoad, _ActionHelpClassPath
-from .typing import is_optional
-from .jsonschema import ActionJsonSchema
 from .optionals import (
     docstring_parser_support,
     import_docstring_parse,
@@ -30,6 +28,7 @@ class SignatureArguments:
         nested_key: Optional[str] = None,
         as_group: bool = True,
         as_positional: bool = False,
+        required: bool = False,
         skip: Optional[Container[str]] = None,
     ) -> int:
         """Adds arguments from a class based on its type hints and docstrings.
@@ -41,6 +40,7 @@ class SignatureArguments:
             nested_key: Key for nested namespace.
             as_group: Whether arguments should be added to a new argument group.
             as_positional: Whether to add required parameters as positional arguments.
+            required: Whether the argument group is required.
             skip: Names of parameters that should be skipped.
 
         Returns:
@@ -57,6 +57,7 @@ class SignatureArguments:
                                              nested_key,
                                              as_group,
                                              as_positional,
+                                             required,
                                              skip,
                                              get_class_init_and_base_docstrings,
                                              get_class_init,
@@ -70,6 +71,7 @@ class SignatureArguments:
         nested_key: Optional[str] = None,
         as_group: bool = True,
         as_positional: bool = False,
+        required: bool = False,
         skip: Optional[Container[str]] = None,
     ) -> int:
         """Adds arguments from a class based on its type hints and docstrings.
@@ -82,6 +84,7 @@ class SignatureArguments:
             nested_key: Key for nested namespace.
             as_group: Whether arguments should be added to a new argument group.
             as_positional: Whether to add required parameters as positional arguments.
+            required: Whether the argument group is required.
             skip: Names of parameters that should be skipped.
 
         Returns:
@@ -103,6 +106,7 @@ class SignatureArguments:
                                              nested_key,
                                              as_group,
                                              as_positional,
+                                             required,
                                              skip,
                                              skip_first=skip_first)
 
@@ -113,6 +117,7 @@ class SignatureArguments:
         nested_key: Optional[str] = None,
         as_group: bool = True,
         as_positional: bool = False,
+        required: bool = False,
         skip: Optional[Container[str]] = None,
     ) -> int:
         """Adds arguments from a function based on its type hints and docstrings.
@@ -124,6 +129,7 @@ class SignatureArguments:
             nested_key: Key for nested namespace.
             as_group: Whether arguments should be added to a new argument group.
             as_positional: Whether to add required parameters as positional arguments.
+            required: Whether the argument group is required.
             skip: Names of parameters that should be skipped.
 
         Returns:
@@ -140,6 +146,7 @@ class SignatureArguments:
                                              nested_key,
                                              as_group,
                                              as_positional,
+                                             required,
                                              skip)
 
 
@@ -149,6 +156,7 @@ class SignatureArguments:
         nested_key: Optional[str],
         as_group: bool,
         as_positional: bool,
+        required: bool,
         skip: Optional[Container[str]],
         docs_func: Callable = lambda x: [x.__doc__],
         sign_func: Callable = lambda x: x,
@@ -161,6 +169,7 @@ class SignatureArguments:
             nested_key: Key for nested namespace.
             as_group: Whether arguments should be added to a new argument group.
             as_positional: Whether to add required parameters as positional arguments.
+            required: Whether the argument group is required.
             skip: Names of parameters that should be skipped.
             docs_func: Function that returns docstrings for a given object.
             sign_func: Function that returns signature method for a given object.
@@ -194,7 +203,7 @@ class SignatureArguments:
         doc_group, doc_params = self._gather_docstrings(objects, docs_func)
 
         ## Create group if requested ##
-        group = self._create_group_if_requested(objects[0], nested_key, as_group, doc_group)
+        group = self._create_group_if_requested(objects[0], nested_key, as_group, required, doc_group)
 
         ## Add objects arguments ##
         added_args = set()
@@ -236,11 +245,9 @@ class SignatureArguments:
                    _issubclass(annotation, (str, int, float)) or \
                    is_pure_dataclass(annotation):
                     kwargs['type'] = annotation
-                elif _issubclass(annotation, Enum):
-                    kwargs['action'] = ActionEnum(enum=annotation)
-                else:
+                elif annotation != inspect._empty:  # type: ignore
                     try:
-                        kwargs['action'] = ActionJsonSchema(annotation=annotation, enable_path=False)
+                        kwargs['action'] = ActionTypeHint(typehint=annotation)
                     except ValueError as ex:
                         self.logger.debug(skip_message+str(ex))  # type: ignore
                 if 'type' in kwargs or 'action' in kwargs:
@@ -263,6 +270,7 @@ class SignatureArguments:
         nested_key: str,
         default: Union[Type, dict] = None,
         as_group: bool = True,
+        required: bool = False,
         **kwargs
     ):
         """Adds arguments from a dataclass based on its field types and docstrings.
@@ -272,6 +280,7 @@ class SignatureArguments:
             nested_key: Key for nested namespace.
             default: Vale for defaults. Must be instance of or kwargs for theclass.
             as_group: Whether arguments should be added to a new argument group.
+            required: Whether the argument group is required.
 
         Returns:
             Number of arguments added.
@@ -288,7 +297,7 @@ class SignatureArguments:
         for key in ['help', 'title']:
             if key in kwargs and kwargs[key] is not None:
                 doc_group = strip_title(kwargs.pop(key))
-        group = self._create_group_if_requested(theclass, nested_key, as_group, doc_group, config_load=True, config_load_type=theclass)
+        group = self._create_group_if_requested(theclass, nested_key, as_group, required, doc_group, config_load=True, config_load_type=theclass)
 
         defaults = {}
         if default is not None:
@@ -318,6 +327,7 @@ class SignatureArguments:
         baseclass: Type,
         nested_key: str,
         as_group: bool = True,
+        required: bool = False,
         metavar: str = '{"class_path":...[,"init_args":...]}',
         help: str = 'Dictionary with "class_path" and "init_args" for any subclass of %(baseclass_name)s.',
         **kwargs
@@ -335,6 +345,7 @@ class SignatureArguments:
             baseclass: Base class to use to check subclasses.
             nested_key: Key for nested namespace.
             as_group: Whether arguments should be added to a new argument group.
+            required: Whether the argument group is required.
             metavar: Variable string to show in the argument's help.
             help: Description of argument to show in the help.
 
@@ -345,7 +356,7 @@ class SignatureArguments:
             raise ValueError('Expected "baseclass" argument to be a class object.')
 
         doc_group = self._gather_docstrings([baseclass], get_class_init_and_base_docstrings)[0]
-        group = self._create_group_if_requested(baseclass, nested_key, as_group, doc_group, config_load=False)
+        group = self._create_group_if_requested(baseclass, nested_key, as_group, required, doc_group, config_load=False)
 
         group.add_argument('--'+nested_key+'.help', action=_ActionHelpClassPath(baseclass=baseclass))
         group.add_argument(
@@ -378,7 +389,12 @@ class SignatureArguments:
         return strip_title(doc_group), doc_params
 
 
-    def _create_group_if_requested(self, obj, nested_key, as_group, doc_group, config_load=True, config_load_type=None):
+    def _create_group_if_requested(self, obj, nested_key, as_group, required, doc_group, config_load=True, config_load_type=None):
+        if required:
+            if nested_key is None:
+                raise ValueError('A nested_key is mandatory to make required.')
+            self.required_args.add(nested_key)
+
         group = self
         if as_group:
             if doc_group is None:
diff --git a/jsonargparse/typehints.py b/jsonargparse/typehints.py
new file mode 100644
index 0000000..6eef78b
--- /dev/null
+++ b/jsonargparse/typehints.py
@@ -0,0 +1,323 @@
+"""Action to support type hints."""
+
+import inspect
+import os
+import re
+import yaml
+from argparse import Action
+from enum import Enum
+from typing import Any, Dict, Iterable, List, Sequence, Set, Tuple, Type, Union
+
+from .actions import _is_action_value_list
+from .typing import registered_types
+from .optionals import (
+    argcomplete_warn_redraw_prompt,
+    files_completer,
+    ModuleNotFound,
+)
+from .util import (
+    namespace_to_dict,
+    import_object,
+    ParserError,
+    Path,
+    yamlParserError,
+    yamlScannerError,
+    _issubclass,
+    _load_config,
+)
+
+
+__all__ = ['ActionTypeHint']
+
+
+root_types = {
+    bool,
+    Any,
+    Union,
+    List, list, Iterable, Sequence,
+    Tuple, tuple,
+    Set, set,
+    Dict, dict,
+}
+
+leaf_types = {
+    str,
+    int,
+    float,
+    bool,
+    type(None),
+}
+
+
+class ActionTypeHint(Action):
+    """Action to parse a type hint."""
+
+    def __init__(
+        self,
+        typehint: Type = None,
+        enable_path: bool = False,
+        **kwargs
+    ):
+        """Initializer for ActionTypeHint instance.
+
+        Args:
+            typehint: The type hint to use for parsing.
+            enable_path: Whether to try to load parsed value from path.
+
+        Raises:
+            ValueError: If a parameter is invalid.
+        """
+        if typehint is not None:
+            if not self.is_supported_typehint(typehint, full=True):
+                raise ValueError('Unsupported type hint '+str(typehint)+'.')
+            self._typehint = typehint
+            self._enable_path = False if is_optional(typehint, Path) else enable_path
+        elif '_typehint' not in kwargs:
+            raise ValueError('Expected typehint keyword argument.')
+        else:
+            self._typehint = kwargs.pop('_typehint')
+            self._enable_path = kwargs.pop('_enable_path')
+            if 'metavar' not in kwargs:
+                kwargs['metavar'] = self._typehint_metavar()
+            super().__init__(**kwargs)
+
+
+    @staticmethod
+    def is_supported_typehint(typehint, full=False):
+        """Whether the given type hint is supported."""
+        supported = \
+            typehint in root_types or \
+            getattr(typehint, '__origin__', None) in root_types or \
+            typehint in registered_types or \
+            _issubclass(typehint, Enum) or \
+            (inspect.isclass(typehint) and typehint not in leaf_types)
+        if full and supported:
+            typehint_origin = getattr(typehint, '__origin__', typehint)
+            if typehint_origin in root_types:
+                for typehint in getattr(typehint, '__args__', []):
+                    if typehint == Ellipsis:
+                        continue
+                    if not (typehint in leaf_types or ActionTypeHint.is_supported_typehint(typehint, full=True)):
+                        return False
+        return supported
+
+
+    @staticmethod
+    def serialize(value, typehint):
+        return adapt_typehints(value, typehint, serialize=True)
+
+
+    def __call__(self, *args, **kwargs):
+        """Parses an argument validating against the corresponding type hint.
+
+        Raises:
+            TypeError: If the argument is not valid.
+        """
+        if len(args) == 0:
+            kwargs['_typehint'] = self._typehint
+            kwargs['_enable_path'] = self._enable_path
+            if 'nargs' in kwargs and kwargs['nargs'] == 0:
+                raise ValueError('ActionTypeHint does not allow nargs=0.')
+            return ActionTypeHint(**kwargs)
+        val = self._check_type(args[2])
+        setattr(args[1], self.dest, val)
+
+
+    def _check_type(self, value, cfg=None):
+        islist = _is_action_value_list(self)
+        if not islist:
+            value = [value]
+        for num, val in enumerate(value):
+            try:
+                orig_val = val
+                val, config_path = _load_config(val, enable_path=self._enable_path, flat_namespace=False)
+                path_meta = val.pop('__path__') if isinstance(val, dict) and '__path__' in val else None
+                try:
+                    val = adapt_typehints(val, self._typehint)
+                except ValueError as ex:
+                    if isinstance(val, (int, float)) and config_path is None:
+                        val = adapt_typehints(orig_val, self._typehint)
+                    else:
+                        raise ex
+                if path_meta is not None:
+                    val['__path__'] = path_meta
+                if isinstance(val, dict) and config_path is not None:
+                    val['__path__'] = config_path
+                value[num] = val
+            except (TypeError, ValueError, yamlParserError, yamlScannerError) as ex:
+                elem = '' if not islist else ' element '+str(num+1)
+                raise TypeError('Parser key "'+self.dest+'"'+elem+': '+str(ex)) from ex
+        return value if islist else value[0]
+
+
+    def _instantiate_classes(self, val):
+        return adapt_typehints(val, self._typehint, instantiate_classes=True)
+
+
+    def _typehint_metavar(self):
+        """Generates a metavar for some types."""
+        metavar = None
+        if self._typehint == bool:
+            metavar = '{true,false}'
+        elif is_optional(self._typehint, bool):
+            metavar = '{true,false,null}'
+        elif _issubclass(self._typehint, Enum):
+            enum = self._typehint
+            metavar = '{'+','.join(list(enum.__members__.keys()))+'}'
+        elif is_optional(self._typehint, Enum):
+            enum = self._typehint.__args__[0]
+            metavar = '{'+','.join(list(enum.__members__.keys())+['null'])+'}'
+        return metavar
+
+
+    def completer(self, prefix, **kwargs):
+        """Used by argcomplete, validates value and shows expected type."""
+        if self._typehint == bool:
+            return ['true', 'false']
+        elif is_optional(self._typehint, bool):
+            return ['true', 'false', 'null']
+        elif _issubclass(self._typehint, Enum):
+            enum = self._typehint
+            return list(enum.__members__.keys())
+        elif is_optional(self._typehint, Enum):
+            enum = self._typehint.__args__[0]
+            return list(enum.__members__.keys())+['null']
+        elif is_optional(self._typehint, Path):
+            return ['null'] + sorted(files_completer(prefix, **kwargs))
+        elif chr(int(os.environ['COMP_TYPE'])) == '?':
+            try:
+                if prefix.strip() == '':
+                    raise ValueError()
+                self._check_type(prefix)
+                msg = 'value already valid, '
+            except (TypeError, ValueError, yamlParserError, yamlScannerError):
+                msg = 'value not yet valid, '
+            msg += 'expected type '+type_to_str(self._typehint)
+            return argcomplete_warn_redraw_prompt(prefix, msg)
+
+
+def adapt_typehints(val, typehint, serialize=False, instantiate_classes=False):
+
+    adapt_kwargs = {'serialize': serialize, 'instantiate_classes': instantiate_classes}
+    subtypehints = getattr(typehint, '__args__', None)
+    typehint_origin = getattr(typehint, '__origin__', typehint)
+
+    # Any
+    if typehint == Any:
+        if isinstance(val, str):
+            val = _load_config(val, enable_path=False, flat_namespace=False)[0]
+
+    # Basic types
+    elif typehint in leaf_types:
+        if not isinstance(val, typehint):
+            raise ValueError('Expected a '+str(typehint)+' but got "'+str(val)+'"')
+
+    # Registered types
+    elif typehint in registered_types:
+        registered_type = registered_types[typehint]
+        if serialize and registered_type.is_value_of_type(val):
+            val = registered_type.serializer(val)
+        elif not serialize and not registered_type.is_value_of_type(val):
+            val = registered_type.deserializer(val)
+
+    # Enum
+    elif _issubclass(typehint, Enum):
+        if serialize and isinstance(val, typehint):
+            val = val.name
+        elif not serialize and not isinstance(val, typehint):
+            val = typehint[val]
+
+    # Union
+    elif typehint_origin == Union:
+        vals = []
+        for subtypehint in subtypehints:
+            try:
+                vals.append(adapt_typehints(val, subtypehint, **adapt_kwargs))
+                break
+            except Exception as ex:
+                vals.append(ex)
+        if all(isinstance(v, Exception) for v in vals):
+            e = ' :: '.join(str(v) for v in vals)
+            raise ValueError('Value "'+str(val)+'" does not validate against any of the types in '+str(typehint)+' :: '+e)
+        val = [v for v in vals if not isinstance(v, Exception)][0]
+
+    # Tuple or Set
+    elif typehint_origin in {Tuple, tuple, Set, set}:
+        if not isinstance(val, (list, tuple, set)):
+            raise ValueError('Expected a '+str(typehint_origin)+' but got "'+str(val)+'"')
+        val = list(val)
+        if subtypehints is not None:
+            is_tuple = typehint_origin in {Tuple, tuple}
+            is_ellipsis = is_ellipsis_tuple(typehint)
+            if is_tuple and not is_ellipsis and len(val) != len(subtypehints):
+                raise ValueError('Expected a tuple with '+str(len(subtypehints))+' elements but got "'+str(val)+'"')
+            for n, v in enumerate(val):
+                subtypehint = subtypehints[0 if is_ellipsis else n]
+                val[n] = adapt_typehints(v, subtypehint, **adapt_kwargs)
+            if is_tuple and len(val) == 0:
+                raise ValueError('Expected a non-empty tuple')
+        if not serialize:
+            val = tuple(val) if typehint_origin in {Tuple, tuple} else set(val)
+
+    # List, Iterable or Sequence
+    elif typehint_origin in {List, list, Iterable, Sequence}:
+        if not isinstance(val, list):
+            raise ValueError('Expected a List but got "'+str(val)+'"')
+        if subtypehints is not None:
+            for n, v in enumerate(val):
+                val[n] = adapt_typehints(v, subtypehints[0], **adapt_kwargs)
+
+    # Dict
+    elif typehint_origin in {Dict, dict}:
+        if not isinstance(val, dict):
+            raise ValueError('Expected a Dict but got "'+str(val)+'"')
+        if subtypehints is not None:
+            if subtypehints[0] == int:
+                cast = str if serialize else int
+                val = {cast(k): v for k, v in val.items()}
+            for k, v in val.items():
+                val[k] = adapt_typehints(v, subtypehints[1], **adapt_kwargs)
+
+    # Subclasses
+    elif not hasattr(typehint, '__origin__') and inspect.isclass(typehint):
+        if not isinstance(val, dict) or 'class_path' not in val:
+            raise ValueError('Expected a Dict with a class_path entry but got "'+str(val)+'"')
+        try:
+            val_class = import_object(val['class_path'])
+            if not _issubclass(val_class, typehint):
+                raise ValueError('"'+val['class_path']+'" is not a subclass of '+typehint.__name__)
+            init_args = {}
+            if 'init_args' in val:
+                from jsonargparse import ArgumentParser
+                parser = ArgumentParser(error_handler=None, parse_as_dict=True)
+                parser.add_class_arguments(val_class)
+                val['init_args'] = parser.parse_object(val['init_args'], defaults=False)
+                if instantiate_classes:
+                    init_args = parser.instantiate_subclasses(val['init_args'])
+            if instantiate_classes:
+                val = val_class(**init_args)
+        except (ImportError, ModuleNotFound, AttributeError, AssertionError, ParserError) as ex:
+            raise ValueError('Problem with given class_path "'+val['class_path']+'" :: '+str(ex)) from ex
+        return val
+
+    return val
+
+
+def is_ellipsis_tuple(typehint):
+    return typehint.__origin__ in {Tuple, tuple} and len(typehint.__args__) > 1 and typehint.__args__[1] == Ellipsis
+
+
+def is_optional(annotation, ref_type):
+    """Checks whether a type annotation is an optional for one type class."""
+    return hasattr(annotation, '__origin__') and \
+        annotation.__origin__ == Union and \
+        len(annotation.__args__) == 2 and \
+        any(type(None) == a for a in annotation.__args__) and \
+        any(_issubclass(a, ref_type) for a in annotation.__args__)
+
+
+def type_to_str(obj):
+    if _issubclass(obj, (bool, int, float, str, Enum)):
+        return obj.__name__
+    elif obj is not None:
+        return re.sub(r'[a-z_.]+\.', '', str(obj)).replace('NoneType', 'null')
diff --git a/jsonargparse/typing.py b/jsonargparse/typing.py
index c20f4c8..4462769 100644
--- a/jsonargparse/typing.py
+++ b/jsonargparse/typing.py
@@ -229,35 +229,34 @@ class RegisteredType:
         type_class: Any,
         serializer: Callable,
         deserializer: Optional[Callable],
-        deserializer_exceptions: Optional[Union[Type[Exception], Tuple[Type[Exception]]]],
+        deserializer_exceptions: Union[Type[Exception], Tuple[Type[Exception], ...]],
         type_check: Callable,
     ):
         self.type_class = type_class
         self.serializer = serializer
-        self.deserializer = type_class if deserializer is None else deserializer
+        self.base_deserializer = type_class if deserializer is None else deserializer
+        self.deserializer_exceptions = deserializer_exceptions
         self.type_check = type_check
-        if deserializer_exceptions is not None:
-            def deserializer_try_except(value):
-                try:
-                    return self.base_deserializer(value)
-                except self.deserializer_exceptions as ex:
-                    raise TypeError(str(ex)) from ex
-            self.base_deserializer = self.deserializer
-            self.deserializer = deserializer_try_except
-            self.deserializer_exceptions = deserializer_exceptions
 
     def __eq__(self, other):
-        return all(getattr(self, k) == getattr(other, k) for k in ['type_class', 'serializer', 'deserializer'])
+        return all(getattr(self, k) == getattr(other, k) for k in ['type_class', 'serializer', 'base_deserializer'])
 
     def is_value_of_type(self, value):
         return self.type_check(value, self.type_class)
 
+    def deserializer(self, value):
+        try:
+            return self.base_deserializer(value)
+        except self.deserializer_exceptions as ex:
+            type_class_name = getattr(self.type_class, '__name__', str(self.type_class))
+            raise ValueError('Not of type '+type_class_name+'. '+str(ex)) from ex
+
 
 def register_type(
     type_class: Any,
     serializer: Callable = str,
     deserializer: Optional[Callable] = None,
-    deserializer_exceptions: Optional[Union[Type[Exception], Tuple[Type[Exception]]]] = None,
+    deserializer_exceptions: Union[Type[Exception], Tuple[Type[Exception], ...]] = (ValueError, TypeError, AttributeError),
     type_check: Callable = lambda v, t: v.__class__ == t,
     uniqueness_key: Optional[Tuple] = None,
 ):
@@ -320,28 +319,4 @@ register_type(
     type_check=lambda v, t: callable(v),
     serializer=lambda x: x.__module__+'.'+x.__name__,
     deserializer=lambda x: x if callable(x) else import_object(x),
-    deserializer_exceptions=AttributeError,
 )
-
-
-def is_optional(annotation, ref_type):
-    """Checks whether a type annotation is an optional for one type class."""
-    return hasattr(annotation, '__origin__') and \
-        annotation.__origin__ == Union and \
-        len(annotation.__args__) == 2 and \
-        any(type(None) == a for a in annotation.__args__) and \
-        any(_issubclass(a, ref_type) for a in annotation.__args__)
-
-
-def type_in(obj, types_set):
-    return obj in types_set or (hasattr(obj, '__origin__') and obj.__origin__ in types_set)
-
-
-def type_to_str(obj):
-    if _issubclass(obj, (bool, int, float, str, Enum)):
-        if hasattr(obj, '_expression'):
-            return obj._type.__name__ + ' ' + obj._expression
-        else:
-            return obj.__name__
-    elif obj is not None:
-        return re.sub(r'[a-z_.]+\.', '', str(obj)).replace('NoneType', 'null')
diff --git a/jsonargparse/util.py b/jsonargparse/util.py
index 40bbb1b..79a0180 100644
--- a/jsonargparse/util.py
+++ b/jsonargparse/util.py
@@ -101,9 +101,14 @@ def _flat_namespace_to_dict(cfg_ns:Namespace) -> Dict[str, Any]:
     def raise_conflicting_base(base):
         raise ParserError('Conflicting namespace base: '+base)
 
+    nested_keys = {k.rsplit('.', 1)[0]+'.' for k in vars(cfg_ns).keys() if '.' in k}
+    skip_keys = {k for k, v in vars(cfg_ns).items() if v is None and k+'.' in nested_keys}
+
     cfg_ns = deepcopy(cfg_ns)
     cfg_dict = {}
     for k, v in vars(cfg_ns).items():
+        if k in skip_keys:
+            continue
         ksplit = k.split('.')
         if len(ksplit) == 1:
             if k in cfg_dict:
@@ -260,6 +265,8 @@ def _issubclass(cls, class_or_tuple):
 
 def import_object(name):
     """Returns an object in a module given its dot import path."""
+    if not isinstance(name, str):
+        raise ValueError('Expected a dot import path string')
     name_module, name_object = name.rsplit('.', 1)
     module = __import__(name_module, fromlist=[name_object])
     return getattr(module, name_object)
diff --git a/setup.cfg b/setup.cfg
index b9d9245..d367b80 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -9,12 +9,12 @@ install_requires =
 [options.extras_require]
 all =
     %(signatures)s
+    %(jsonschema)s
     %(jsonnet)s
     %(urls)s
     %(argcomplete)s
     %(reconplogger)s
 signatures =
-    %(jsonschema)s
     docstring-parser>=0.7.3
 jsonschema =
     jsonschema>=3.2.0
@@ -108,6 +108,7 @@ envlist = py{35,36,37,38,39},pypy3
 extras =
     test
     signatures
+    jsonschema
     urls
     argcomplete
 changedir = jsonargparse_tests


Test patch:
diff --git a/jsonargparse_tests/actions_tests.py b/jsonargparse_tests/actions_tests.py
index 23db878..54f9d71 100755
--- a/jsonargparse_tests/actions_tests.py
+++ b/jsonargparse_tests/actions_tests.py
@@ -2,7 +2,6 @@
 
 import json
 import pathlib
-from enum import Enum
 from io import StringIO
 from jsonargparse_tests.base import *
 from jsonargparse import ArgumentParser
@@ -63,88 +62,6 @@ class SimpleActionsTests(unittest.TestCase):
         self.assertRaises(ParserError, lambda: parser.parse_args(['--val', '1']))
 
 
-    def test_ActionEnum(self):
-
-        class MyEnum(Enum):
-            A = 1
-            B = 2
-            C = 3
-
-        parser = ArgumentParser(error_handler=None)
-        parser.add_argument('--enum',
-            action=ActionEnum(enum=MyEnum),
-            default=MyEnum.C,
-            help='Description')
-
-        for val in ['A', 'B', 'C']:
-            self.assertEqual(MyEnum[val], parser.parse_args(['--enum='+val]).enum)
-        for val in ['X', 'b', 2]:
-            self.assertRaises(ParserError, lambda: parser.parse_args(['--enum='+str(val)]))
-
-        cfg = parser.parse_args(['--enum=C'], with_meta=False)
-        self.assertEqual('enum: C\n', parser.dump(cfg))
-
-        help_str = StringIO()
-        parser.print_help(help_str)
-        self.assertIn('Description (type: MyEnum, default: C)', help_str.getvalue())
-
-        def func(a1: MyEnum = MyEnum['A']):
-            return a1
-
-        parser = ArgumentParser()
-        parser.add_function_arguments(func)
-        self.assertEqual(MyEnum['A'], parser.get_defaults().a1)
-        self.assertEqual(MyEnum['B'], parser.parse_args(['--a1=B']).a1)
-
-        self.assertRaises(ValueError, lambda: ActionEnum())
-        self.assertRaises(ValueError, lambda: ActionEnum(enum=object))
-        self.assertRaises(ValueError, lambda: parser.add_argument('--bad', type=MyEnum, action=True))
-
-
-    def test_ActionOperators(self):
-        parser = ArgumentParser(prog='app', error_handler=None)
-        parser.add_argument('--le0',
-            action=ActionOperators(expr=('<', 0)))
-        parser.add_argument('--gt1.a.le4',
-            action=ActionOperators(expr=[('>', 1.0), ('<=', 4.0)], join='and', type=float))
-        parser.add_argument('--lt5.o.ge10.o.eq7',
-            action=ActionOperators(expr=[('<', 5), ('>=', 10), ('==', 7)], join='or', type=int))
-        parser.add_argument('--ge0',
-            nargs=3,
-            action=ActionOperators(expr=('>=', 0)))
-
-        self.assertEqual(1.5, parser.parse_args(['--gt1.a.le4', '1.5']).gt1.a.le4)
-        self.assertEqual(4.0, parser.parse_args(['--gt1.a.le4', '4.0']).gt1.a.le4)
-        self.assertRaises(ParserError, lambda: parser.parse_args(['--gt1.a.le4', '1.0']))
-        self.assertRaises(ParserError, lambda: parser.parse_args(['--gt1.a.le4', '5.5']))
-
-        self.assertEqual(1.5, parser.parse_string('gt1:\n  a:\n    le4: 1.5').gt1.a.le4)
-        self.assertEqual(4.0, parser.parse_string('gt1:\n  a:\n    le4: 4.0').gt1.a.le4)
-        self.assertRaises(ParserError, lambda: parser.parse_string('gt1:\n  a:\n    le4: 1.0'))
-        self.assertRaises(ParserError, lambda: parser.parse_string('gt1:\n  a:\n    le4: 5.5'))
-
-        self.assertEqual(1.5, parser.parse_env({'APP_GT1__A__LE4': '1.5'}).gt1.a.le4)
-        self.assertEqual(4.0, parser.parse_env({'APP_GT1__A__LE4': '4.0'}).gt1.a.le4)
-        self.assertRaises(ParserError, lambda: parser.parse_env({'APP_GT1__A__LE4': '1.0'}))
-        self.assertRaises(ParserError, lambda: parser.parse_env({'APP_GT1__A__LE4': '5.5'}))
-
-        self.assertEqual(2, parser.parse_args(['--lt5.o.ge10.o.eq7', '2']).lt5.o.ge10.o.eq7)
-        self.assertEqual(7, parser.parse_args(['--lt5.o.ge10.o.eq7', '7']).lt5.o.ge10.o.eq7)
-        self.assertEqual(10, parser.parse_args(['--lt5.o.ge10.o.eq7', '10']).lt5.o.ge10.o.eq7)
-        self.assertRaises(ParserError, lambda: parser.parse_args(['--lt5.o.ge10.o.eq7', '5']))
-        self.assertRaises(ParserError, lambda: parser.parse_args(['--lt5.o.ge10.o.eq7', '8']))
-
-        self.assertEqual([0, 1, 2], parser.parse_args(['--ge0', '0', '1', '2']).ge0)
-
-        self.assertRaises(ValueError, lambda: parser.add_argument('--op1', action=ActionOperators))
-        action = ActionOperators(expr=('<', 0))
-        self.assertRaises(ValueError, lambda: parser.add_argument('--op2', type=float, action=action))
-        self.assertRaises(ValueError, lambda: parser.add_argument('--op3', nargs=0, action=action))
-        self.assertRaises(ValueError, lambda: ActionOperators())
-        self.assertRaises(ValueError, lambda: ActionOperators(expr='<'))
-        self.assertRaises(ValueError, lambda: ActionOperators(expr=[('<', 5), ('>=', 10)], join='xor'))
-
-
 class ActionPathTests(TempDirTestCase):
 
     def test_ActionPath(self):
diff --git a/jsonargparse_tests/argcomplete_tests.py b/jsonargparse_tests/argcomplete_tests.py
index dd1d493..33b4ac3 100755
--- a/jsonargparse_tests/argcomplete_tests.py
+++ b/jsonargparse_tests/argcomplete_tests.py
@@ -146,7 +146,6 @@ class ArgcompleteTests(TempDirTestCase):
         self.assertEqual(out.getvalue(), b'abc\x0babd')
 
 
-    @unittest.skipIf(not jsonschema_support, 'jsonschema package is required')
     def test_optional(self):
         class MyEnum(Enum):
             A = 1
@@ -171,11 +170,9 @@ class ArgcompleteTests(TempDirTestCase):
     @unittest.skipIf(platform.python_implementation() != 'CPython', 'only CPython supported')
     def test_json(self):
         self.parser.add_argument('--json', action=ActionJsonSchema(schema={'type': 'object'}))
-        self.parser.add_argument('--list', type=List[int])
 
         for arg, expected in [('--json=1',            'value not yet valid'),
-                              ("--json='{\"a\": 1}'", 'value already valid'),
-                              ("--list='[1, 2, 3]'",  'value already valid, expected type List[int]')]:
+                              ("--json='{\"a\": 1}'", 'value already valid')]:
             os.environ['COMP_LINE'] = 'tool.py '+arg
             os.environ['COMP_POINT'] = str(len(os.environ['COMP_LINE']))
 
@@ -203,7 +200,28 @@ class ArgcompleteTests(TempDirTestCase):
         self.assertIn('value not yet valid', out.getvalue().decode('utf-8').replace('\xa0', ' ').replace('_', ' '))
 
 
-    @unittest.skipIf(not jsonschema_support, 'jsonschema package is required')
+    def test_list(self):
+        self.parser.add_argument('--list', type=List[int])
+
+        os.environ['COMP_LINE'] = "tool.py --list='[1, 2, 3]'"
+        os.environ['COMP_POINT'] = str(len(os.environ['COMP_LINE']))
+
+        out, err = BytesIO(), StringIO()
+        with redirect_stdout(out), redirect_stderr(err), self.assertRaises(SystemExit):
+            self.argcomplete.autocomplete(self.parser, exit_method=sys.exit, output_stream=sys.stdout)
+        self.assertEqual(out.getvalue(), b'')
+        self.assertIn('value already valid, expected type List[int]', err.getvalue())
+
+        os.environ['COMP_LINE'] = 'tool.py --list='
+        os.environ['COMP_POINT'] = str(len(os.environ['COMP_LINE']))
+
+        out, err = BytesIO(), StringIO()
+        with redirect_stdout(out), redirect_stderr(err), self.assertRaises(SystemExit):
+            self.argcomplete.autocomplete(self.parser, exit_method=sys.exit, output_stream=sys.stdout)
+        self.assertEqual(err.getvalue(), '')
+        self.assertIn('value not yet valid', out.getvalue().decode('utf-8').replace('\xa0', ' ').replace('_', ' '))
+
+
     def test_bool(self):
         self.parser.add_argument('--bool', type=bool)
         os.environ['COMP_LINE'] = 'tool.py --bool='
@@ -215,7 +233,6 @@ class ArgcompleteTests(TempDirTestCase):
         self.assertEqual(b'true\x0bfalse', out.getvalue())
 
 
-    @unittest.skipIf(not jsonschema_support, 'jsonschema package is required')
     @unittest.skipIf(os.name != 'posix', 'Path class currently only supported in posix systems')
     def test_optional_path(self):
         self.parser.add_argument('--path', type=Optional[Path_fr])
diff --git a/jsonargparse_tests/core_tests.py b/jsonargparse_tests/core_tests.py
index fd3a7d3..9de3951 100755
--- a/jsonargparse_tests/core_tests.py
+++ b/jsonargparse_tests/core_tests.py
@@ -706,7 +706,7 @@ class ConfigFilesTests(TempDirTestCase):
         with open(default_config_file, 'w') as output_file:
             output_file.write('op1: from yaml\n')
 
-        parser = ArgumentParser(default_config_files=[default_config_file])
+        parser = ArgumentParser(default_config_files=[default_config_file], error_handler=None)
         parser.add_argument('--op1', default='from default')
 
         self.assertEqual(parser.get_default('op1'), 'from yaml')
diff --git a/jsonargparse_tests/deprecated_tests.py b/jsonargparse_tests/deprecated_tests.py
new file mode 100755
index 0000000..f404329
--- /dev/null
+++ b/jsonargparse_tests/deprecated_tests.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python3
+
+from enum import Enum
+from io import StringIO
+from jsonargparse_tests.base import *
+from jsonargparse import ArgumentParser
+
+
+class DeprecatedTests(unittest.TestCase):
+
+    def test_ActionEnum(self):
+
+        class MyEnum(Enum):
+            A = 1
+            B = 2
+            C = 3
+
+        parser = ArgumentParser(error_handler=None)
+        action = ActionEnum(enum=MyEnum)
+        parser.add_argument('--enum',
+            action=action,
+            default=MyEnum.C,
+            help='Description')
+
+        for val in ['A', 'B', 'C']:
+            self.assertEqual(MyEnum[val], parser.parse_args(['--enum='+val]).enum)
+        for val in ['X', 'b', 2]:
+            self.assertRaises(ParserError, lambda: parser.parse_args(['--enum='+str(val)]))
+
+        cfg = parser.parse_args(['--enum=C'], with_meta=False)
+        self.assertEqual('enum: C\n', parser.dump(cfg))
+
+        help_str = StringIO()
+        parser.print_help(help_str)
+        self.assertIn('Description (type: MyEnum, default: C)', help_str.getvalue())
+
+        def func(a1: MyEnum = MyEnum['A']):
+            return a1
+
+        parser = ArgumentParser()
+        parser.add_function_arguments(func)
+        self.assertEqual(MyEnum['A'], parser.get_defaults().a1)
+        self.assertEqual(MyEnum['B'], parser.parse_args(['--a1=B']).a1)
+
+        self.assertRaises(ValueError, lambda: ActionEnum())
+        self.assertRaises(ValueError, lambda: ActionEnum(enum=object))
+        self.assertRaises(ValueError, lambda: parser.add_argument('--bad1', type=MyEnum, action=True))
+        self.assertRaises(ValueError, lambda: parser.add_argument('--bad2', type=float, action=action))
+
+
+    def test_ActionOperators(self):
+        parser = ArgumentParser(prog='app', error_handler=None)
+        parser.add_argument('--le0',
+            action=ActionOperators(expr=('<', 0)))
+        parser.add_argument('--gt1.a.le4',
+            action=ActionOperators(expr=[('>', 1.0), ('<=', 4.0)], join='and', type=float))
+        parser.add_argument('--lt5.o.ge10.o.eq7',
+            action=ActionOperators(expr=[('<', 5), ('>=', 10), ('==', 7)], join='or', type=int))
+        parser.add_argument('--ge0',
+            nargs=3,
+            action=ActionOperators(expr=('>=', 0)))
+
+        self.assertEqual(1.5, parser.parse_args(['--gt1.a.le4', '1.5']).gt1.a.le4)
+        self.assertEqual(4.0, parser.parse_args(['--gt1.a.le4', '4.0']).gt1.a.le4)
+        self.assertRaises(ParserError, lambda: parser.parse_args(['--gt1.a.le4', '1.0']))
+        self.assertRaises(ParserError, lambda: parser.parse_args(['--gt1.a.le4', '5.5']))
+
+        self.assertEqual(1.5, parser.parse_string('gt1:\n  a:\n    le4: 1.5').gt1.a.le4)
+        self.assertEqual(4.0, parser.parse_string('gt1:\n  a:\n    le4: 4.0').gt1.a.le4)
+        self.assertRaises(ParserError, lambda: parser.parse_string('gt1:\n  a:\n    le4: 1.0'))
+        self.assertRaises(ParserError, lambda: parser.parse_string('gt1:\n  a:\n    le4: 5.5'))
+
+        self.assertEqual(1.5, parser.parse_env({'APP_GT1__A__LE4': '1.5'}).gt1.a.le4)
+        self.assertEqual(4.0, parser.parse_env({'APP_GT1__A__LE4': '4.0'}).gt1.a.le4)
+        self.assertRaises(ParserError, lambda: parser.parse_env({'APP_GT1__A__LE4': '1.0'}))
+        self.assertRaises(ParserError, lambda: parser.parse_env({'APP_GT1__A__LE4': '5.5'}))
+
+        self.assertEqual(2, parser.parse_args(['--lt5.o.ge10.o.eq7', '2']).lt5.o.ge10.o.eq7)
+        self.assertEqual(7, parser.parse_args(['--lt5.o.ge10.o.eq7', '7']).lt5.o.ge10.o.eq7)
+        self.assertEqual(10, parser.parse_args(['--lt5.o.ge10.o.eq7', '10']).lt5.o.ge10.o.eq7)
+        self.assertRaises(ParserError, lambda: parser.parse_args(['--lt5.o.ge10.o.eq7', '5']))
+        self.assertRaises(ParserError, lambda: parser.parse_args(['--lt5.o.ge10.o.eq7', '8']))
+
+        self.assertEqual([0, 1, 2], parser.parse_args(['--ge0', '0', '1', '2']).ge0)
+
+        self.assertRaises(ValueError, lambda: parser.add_argument('--op1', action=ActionOperators))
+        action = ActionOperators(expr=('<', 0))
+        self.assertRaises(ValueError, lambda: parser.add_argument('--op2', type=float, action=action))
+        self.assertRaises(ValueError, lambda: parser.add_argument('--op3', nargs=0, action=action))
+        self.assertRaises(ValueError, lambda: ActionOperators())
+        self.assertRaises(ValueError, lambda: ActionOperators(expr='<'))
+        self.assertRaises(ValueError, lambda: ActionOperators(expr=[('<', 5), ('>=', 10)], join='xor'))
+
+
+if __name__ == '__main__':
+    unittest.main(verbosity=2)
diff --git a/jsonargparse_tests/jsonschema_tests.py b/jsonargparse_tests/jsonschema_tests.py
index 447e2e3..8893f43 100755
--- a/jsonargparse_tests/jsonschema_tests.py
+++ b/jsonargparse_tests/jsonschema_tests.py
@@ -104,7 +104,6 @@ class JsonSchemaTests(TempDirTestCase):
     def test_ActionJsonSchema_failures(self):
         self.assertRaises(ValueError, lambda: ActionJsonSchema())
         self.assertRaises(ValueError, lambda: ActionJsonSchema(schema=':'+json.dumps(schema1)))
-        self.assertRaises(ValueError, lambda: ActionJsonSchema(schema=schema1, annotation=int))
 
 
     def test_ActionJsonSchema_help(self):
diff --git a/jsonargparse_tests/signatures_tests.py b/jsonargparse_tests/signatures_tests.py
index 582faaa..190a362 100755
--- a/jsonargparse_tests/signatures_tests.py
+++ b/jsonargparse_tests/signatures_tests.py
@@ -1,18 +1,17 @@
 #!/usr/bin/env python3
 
+import calendar
 import json
 import yaml
-import calendar
 from enum import Enum
 from io import StringIO
 from contextlib import redirect_stdout
-from typing import Dict, List, Tuple, Optional, Union, Any
+from typing import Any, Dict, List, Optional, Tuple, Union
 from jsonargparse_tests.base import *
 from jsonargparse.actions import _find_action
 from jsonargparse.util import _suppress_stderr
 
 
-@unittest.skipIf(not jsonschema_support, 'jsonschema package is required')
 class SignaturesTests(unittest.TestCase):
 
     def test_add_class_arguments(self):
@@ -109,7 +108,6 @@ class SignaturesTests(unittest.TestCase):
         self.assertEqual('a', Class3(**namespace_to_dict(cfg))())
 
         self.assertRaises(ParserError, lambda: parser.parse_args([]))  # c3_a0 is required
-        self.assertRaises(ParserError, lambda: parser.parse_args(['--c3_a0=0', '--c3_a4=4.0']))  # c3_a4 is str or None
         self.assertRaises(ParserError, lambda: parser.parse_args(['--c3_a0=0', '--c3_a7=["3", "3", 3.0]']))  # tuple[1] is int
 
         if docstring_parser_support:
@@ -263,8 +261,22 @@ class SignaturesTests(unittest.TestCase):
         self.assertIn('--cal.init_args.firstweekday', out.getvalue())
 
 
+    def test_required_group(self):
+        parser = ArgumentParser(error_handler=None)
+        parser.add_subclass_arguments(calendar.Calendar, 'cal', required=True)
+        self.assertRaises(ParserError, lambda: parser.parse_args([]))
+        self.assertRaises(ValueError, lambda: parser.add_class_arguments(calendar.Calendar, required=True))
+
+
+    def test_invalid_type(self):
+
+        def func(a1: None):
+            return a1
+
+        parser = ArgumentParser(error_handler=None)
+        self.assertRaises(ValueError, lambda: parser.add_function_arguments(func))
+
 
-    @unittest.skipIf(not jsonschema_support, 'jsonschema package is required')
     def test_optional_enum(self):
 
         class MyEnum(Enum):
@@ -442,7 +454,6 @@ class SignaturesTests(unittest.TestCase):
             self.assertNotIn('a1 description', help_str.getvalue())
 
 
-@unittest.skipIf(not jsonschema_support, 'jsonschema package is required')
 class SignaturesConfigTests(TempDirTestCase):
 
     def test_add_function_arguments_config(self):
diff --git a/jsonargparse_tests/signatures_tests_gt_py35.py b/jsonargparse_tests/signatures_tests_gt_py35.py
index d886564..53c8bdf 100755
--- a/jsonargparse_tests/signatures_tests_gt_py35.py
+++ b/jsonargparse_tests/signatures_tests_gt_py35.py
@@ -1,11 +1,12 @@
 #!/usr/bin/env python3
 
+import yaml
 from io import StringIO
 from typing import Dict, List
 from jsonargparse_tests.base import *
 
 
-@unittest.skipIf(not jsonschema_support or not dataclasses_support, 'jsonschema and dataclasses packages are required')
+@unittest.skipIf(not dataclasses_support, 'dataclasses packages are required')
 class SignaturesTests(unittest.TestCase):
 
     @classmethod
@@ -38,20 +39,27 @@ class SignaturesTests(unittest.TestCase):
         cls.MyDataClassB = MyDataClassB
 
 
+    @unittest.skipIf(not docstring_parser_support, 'docstring-parser package is required')
     def test_add_dataclass_arguments(self):
-        parser = ArgumentParser()
+        parser = ArgumentParser(error_handler=None)
         parser.add_dataclass_arguments(self.MyDataClassA, 'a', default=self.MyDataClassA(), title='CustomA title')
         parser.add_dataclass_arguments(self.MyDataClassB, 'b', default=self.MyDataClassB())
 
         cfg = namespace_to_dict(parser.get_defaults())
-        self.assertEqual({'a1': 1, 'a2': '2'}, cfg['a'])
-        self.assertEqual({'b1': 3.0, 'b2': {'a1': 1, 'a2': '2'}}, cfg['b'])
+        self.assertEqual(self.dataclasses.asdict(self.MyDataClassA()), cfg['a'])
+        self.assertEqual(self.dataclasses.asdict(self.MyDataClassB()), cfg['b'])
+        dump = yaml.safe_load(parser.dump(cfg))
+        self.assertEqual(self.dataclasses.asdict(self.MyDataClassA()), dump['a'])
+        self.assertEqual(self.dataclasses.asdict(self.MyDataClassB()), dump['b'])
 
         cfg = parser.instantiate_subclasses(cfg)
         self.assertIsInstance(cfg.a, self.MyDataClassA)
         self.assertIsInstance(cfg.b, self.MyDataClassB)
         self.assertIsInstance(cfg.b.b2, self.MyDataClassA)
 
+        self.assertEqual(5, parser.parse_args(['--b.b2.a1=5']).b.b2.a1)
+        self.assertRaises(ParserError, lambda: parser.parse_args(['--b.b2.a1=x']))
+
         help_str = StringIO()
         parser.print_help(help_str)
         self.assertIn('CustomA title:', help_str.getvalue())
@@ -64,14 +72,69 @@ class SignaturesTests(unittest.TestCase):
         with self.assertRaises(ValueError):
             parser.add_dataclass_arguments(self.MyDataClassB, 'c', default=self.MyDataClassB(b2=self.MyDataClassB()))
 
-        class MyClass(ArgumentParser, self.MyDataClassA):
-            pass
+        class MyClass(int, self.MyDataClassA):
+            """MyClass description"""
 
         with self.assertRaises(ValueError):
             parser.add_dataclass_arguments(MyClass, 'c')
 
 
-    def test_dataclass_type(self):
+    def test_dataclass_typehint(self):
+
+        class MyClass:
+            def __init__(
+                self,
+                a1: self.MyDataClassA = self.MyDataClassA(),  # type: ignore
+                a2: self.MyDataClassB = self.MyDataClassB(),  # type: ignore
+            ):
+                """MyClass description"""
+
+        parser = ArgumentParser()
+        parser.add_class_arguments(MyClass, 'g')
+
+        cfg = namespace_to_dict(parser.get_defaults())
+        self.assertEqual(self.dataclasses.asdict(self.MyDataClassA()), cfg['g']['a1'])
+        self.assertEqual(self.dataclasses.asdict(self.MyDataClassB()), cfg['g']['a2'])
+        dump = yaml.safe_load(parser.dump(cfg))
+        self.assertEqual(self.dataclasses.asdict(self.MyDataClassA()), dump['g']['a1'])
+        self.assertEqual(self.dataclasses.asdict(self.MyDataClassB()), dump['g']['a2'])
+
+        cfg = parser.instantiate_subclasses(cfg)
+        self.assertIsInstance(cfg.g.a1, self.MyDataClassA)
+        self.assertIsInstance(cfg.g.a2, self.MyDataClassB)
+        self.assertIsInstance(cfg.g.a2.b2, self.MyDataClassA)
+
+
+    def test_dataclass_typehint_in_subclass(self):
+
+        class MyClass1:
+            def __init__(self, a1: self.MyDataClassB = self.MyDataClassB()):  # type: ignore
+                """MyClass1 description"""
+                self.a1 = a1
+
+        class MyClass2:
+            def __init__(self, c1: MyClass1):
+                """MyClass2 description"""
+                self.c1 = c1
+
+        parser = ArgumentParser(error_handler=None)
+        parser.add_class_arguments(MyClass2)
+
+        from jsonargparse_tests import signatures_tests_gt_py35
+        setattr(signatures_tests_gt_py35, 'MyClass1', MyClass1)
+
+        class_path = '"class_path": "jsonargparse_tests.signatures_tests_gt_py35.MyClass1"'
+        init_args = '"init_args": {"a1": {"b2": {"a1": 7}}}'
+        cfg = parser.parse_args(['--c1={'+class_path+', '+init_args+'}'])
+        self.assertEqual(cfg.c1.class_path, 'jsonargparse_tests.signatures_tests_gt_py35.MyClass1')
+        self.assertEqual(cfg.c1.init_args.a1.b2.a1, 7)
+        cfg = parser.instantiate_subclasses(cfg)
+        self.assertIsInstance(cfg.c1, MyClass1)
+        self.assertIsInstance(cfg.c1.a1, self.MyDataClassB)
+        self.assertIsInstance(cfg.c1.a1.b2, self.MyDataClassA)
+
+
+    def test_dataclass_add_argument_type(self):
         parser = ArgumentParser()
         parser.add_argument('--b', type=self.MyDataClassB, default=self.MyDataClassB(b1=-3.0))
 
diff --git a/jsonargparse_tests/typehints_tests.py b/jsonargparse_tests/typehints_tests.py
index 7ce7ae4..b46d179 100755
--- a/jsonargparse_tests/typehints_tests.py
+++ b/jsonargparse_tests/typehints_tests.py
@@ -1,16 +1,17 @@
 #!/usr/bin/env python3
 # pylint: disable=unsubscriptable-object
 
-import uuid
+import json
 import pathlib
+import uuid
 from calendar import Calendar
 from datetime import datetime
 from enum import Enum
-from typing import Callable, Dict, Generator, List, Optional, Tuple, Union
+from typing import Any, Callable, Dict, List, Optional, Tuple, Union
 from jsonargparse_tests.base import *
+from jsonargparse.typehints import is_optional
 
 
-@unittest.skipIf(not jsonschema_support, 'jsonschema package is required')
 class TypeHintsTests(unittest.TestCase):
 
     def test_add_argument_type_hint(self):
@@ -24,6 +25,7 @@ class TypeHintsTests(unittest.TestCase):
         self.assertRaises(ParserError, lambda: parser.parse_args(['--op1', '4.5']))
         parser.add_argument('--op2', type=Optional[Email])
         self.assertEqual('a@b.c', parser.parse_args(['--op2', 'a@b.c']).op2)
+        self.assertIsNone(parser.parse_args(['--op2=null']).op2)
         self.assertRaises(ParserError, lambda: parser.parse_args(['--op2', 'abc']))
 
 
@@ -63,6 +65,7 @@ class TypeHintsTests(unittest.TestCase):
         self.assertEqual('', parser.parse_args(['--op', '']).op)
         self.assertEqual(' abc ', parser.parse_args(['--op= abc ']).op)
         self.assertEqual(' ', parser.parse_args(['--cfg={"op":" "}']).op)
+        self.assertIsNone(parser.parse_args(['--op=null']).op)
 
 
     def test_list_path(self):
@@ -93,6 +96,7 @@ class TypeHintsTests(unittest.TestCase):
         parser.add_argument('--list2', type=List[Union[int, MyEnum]])
         self.assertEqual([1.2, 'ab'], parser.parse_args(['--list1=[1.2, "ab"]']).list1)
         self.assertEqual([3, MyEnum.ab], parser.parse_args(['--list2=[3, "ab"]']).list2)
+        self.assertRaises(ParserError, lambda: parser.parse_args(['--list1={"a":1, "b":"2"}']))
 
 
     def test_dict(self):
@@ -115,6 +119,7 @@ class TypeHintsTests(unittest.TestCase):
         self.assertEqual({'a': True, 'b': 'f'}, cfg['dict2'])
         self.assertIsInstance(cfg['dict2']['b'], Path)
         self.assertEqual({5: None}, parser.parse_args(['--dict1={"5":null}'])['dict1'])
+        self.assertRaises(ParserError, lambda: parser.parse_args(['--dict1=["a", "b"]']))
 
 
     def test_tuple(self):
@@ -130,6 +135,7 @@ class TypeHintsTests(unittest.TestCase):
         self.assertRaises(ParserError, lambda: parser.parse_args(['--tuple=[2, "a", "b", 5]']))
         self.assertRaises(ParserError, lambda: parser.parse_args(['--tuple=[2, "a"]']))
         self.assertRaises(ParserError, lambda: parser.parse_args(['--tuple=["2", "a", "b"]']))
+        self.assertRaises(ParserError, lambda: parser.parse_args(['--tuple={"a":1, "b":"2"}']))
 
 
     def test_nested_tuples(self):
@@ -168,6 +174,21 @@ class TypeHintsTests(unittest.TestCase):
         self.assertEqual(parser.dump(cfg), 'complex: (2+3j)\n')
 
 
+    def test_type_Any(self):
+        parser = ArgumentParser(error_handler=None, parse_as_dict=True)
+        parser.add_argument('--any', type=Any)
+        self.assertEqual('abc', parser.parse_args(['--any=abc'])['any'])
+        self.assertEqual(123, parser.parse_args(['--any=123'])['any'])
+        self.assertEqual(5.6, parser.parse_args(['--any=5.6'])['any'])
+        self.assertEqual([7, 8], parser.parse_args(['--any=[7, 8]'])['any'])
+        self.assertEqual({"a":0, "b":1}, parser.parse_args(['--any={"a":0, "b":1}'])['any'])
+        self.assertTrue(parser.parse_args(['--any=True'])['any'])
+        self.assertFalse(parser.parse_args(['--any=False'])['any'])
+        self.assertIsNone(parser.parse_args(['--any=null'])['any'])
+        self.assertEqual(' ', parser.parse_args(['--any= '])['any'])
+        self.assertEqual(' xyz ', parser.parse_args(['--any= xyz '])['any'])
+
+
     def test_uuid(self):
         id1 = uuid.uuid4()
         id2 = uuid.uuid4()
@@ -208,6 +229,8 @@ class TypeHintsTests(unittest.TestCase):
             parser.parse_args(['--op=[{"class_path": "jsonargparse.NotExist"}]'])
         with self.assertRaises(ParserError):
             parser.parse_args(['--op=[{"class_path": "jsonargparse0.IncorrectModule"}]'])
+        with self.assertRaises(ParserError):
+            parser.parse_args(['--op=["calendar.Calendar"]'])
 
         init_args = '"init_args": {"bad_arg": True}'
         with self.assertRaises(ParserError):
@@ -236,8 +259,8 @@ class TypeHintsTests(unittest.TestCase):
 
 
     def test_unsupported_type(self):
-        parser = ArgumentParser(error_handler=None)
-        self.assertRaises(ValueError, lambda: parser.add_argument('--op', type=Optional[Generator]))
+        self.assertRaises(ValueError, lambda: ActionTypeHint(typehint=lambda: None))
+        self.assertRaises(ValueError, lambda: ActionTypeHint(typehint=Union[int, lambda: None]))
 
 
     def test_register_type(self):
@@ -258,7 +281,6 @@ class TypeHintsTests(unittest.TestCase):
         register_type(uuid.UUID)
 
 
-@unittest.skipIf(not jsonschema_support, 'jsonschema package is required')
 class TypeHintsTmpdirTests(TempDirTestCase):
 
     def test_optional_path(self):
@@ -272,5 +294,71 @@ class TypeHintsTmpdirTests(TempDirTestCase):
         self.assertRaises(ParserError, lambda: parser.parse_args(['--path=not_exist']))
 
 
+    def test_enable_path(self):
+        data = {'a': 1, 'b': 2, 'c': [3, 4]}
+        cal = {'class_path': 'calendar.Calendar'}
+        with open('data.yaml', 'w') as f:
+            json.dump(data, f)
+        with open('cal.yaml', 'w') as f:
+            json.dump(cal, f)
+
+        parser = ArgumentParser(parse_as_dict=True)
+        parser.add_argument('--data', type=Dict[str, Any], enable_path=True)
+        parser.add_argument('--cal', type=Calendar, enable_path=True)
+        cfg = parser.parse_args(['--data=data.yaml'])
+        self.assertEqual('data.yaml', str(cfg['data'].pop('__path__')))
+        self.assertEqual(data, cfg['data'])
+        cfg = parser.instantiate_subclasses(parser.parse_args(['--cal=cal.yaml']))
+        self.assertIsInstance(cfg['cal'], Calendar)
+
+
+    def test_class_type_with_default_config_files(self):
+        config = {
+            'class_path': 'calendar.Calendar',
+            'init_args': {'firstweekday': 3},
+        }
+        config_path = os.path.join(self.tmpdir, 'config.yaml')
+        with open(config_path, 'w') as f:
+            json.dump({'data': {'cal': config}}, f)
+
+        class MyClass:
+            def __init__(self, cal: Optional[Calendar] = None, val: int = 2):
+                self.cal = cal
+
+        parser = ArgumentParser(error_handler=None, default_config_files=[config_path])
+        parser.add_argument('--op', default='from default')
+        parser.add_class_arguments(MyClass, 'data')
+
+        cfg = namespace_to_dict(parser.get_defaults())
+        self.assertEqual(config_path, str(cfg['__default_config__']))
+        self.assertEqual(cfg['data']['cal'], config)
+        cfg = parser.dump(cfg)
+        self.assertIn('class_path: calendar.Calendar\n', cfg)
+        self.assertIn('firstweekday: 3\n', cfg)
+
+
+class OtherTests(unittest.TestCase):
+
+    def test_is_optional(self):
+        class MyEnum(Enum):
+            A = 1
+
+        params = [
+            (Optional[bool],             bool, True),
+            (Union[type(None), bool],    bool, True),
+            (Dict[bool, type(None)],     bool, False),
+            (Optional[Path_fr],          Path, True),
+            (Union[type(None), Path_fr], Path, True),
+            (Dict[Path_fr, type(None)],  Path, False),
+            (Optional[MyEnum],           Enum, True),
+            (Union[type(None), MyEnum],  Enum, True),
+            (Dict[MyEnum, type(None)],   Enum, False),
+        ]
+
+        for typehint, ref_type, expected in params:
+            with self.subTest(str(typehint)):
+                self.assertEqual(expected, is_optional(typehint, ref_type))
+
+
 if __name__ == '__main__':
     unittest.main(verbosity=2)
diff --git a/jsonargparse_tests/typing_tests.py b/jsonargparse_tests/typing_tests.py
index ee1b409..116dc86 100755
--- a/jsonargparse_tests/typing_tests.py
+++ b/jsonargparse_tests/typing_tests.py
@@ -4,9 +4,8 @@ import os
 import pickle
 import pathlib
 import jsonargparse.typing
-from enum import Enum
-from typing import Optional, Union, Dict
-from jsonargparse.typing import is_optional, type_to_str, RegisteredType
+from typing import Callable
+from jsonargparse.typing import RegisteredType
 from jsonargparse_tests.base import *
 
 
@@ -151,7 +150,6 @@ class PathTypeTests(TempDirTestCase):
         self.assertEqual(repr(path), self.file_fr)
         self.assertEqual(path(), os.path.realpath(self.file_fr))
         self.assertRaises(TypeError, lambda: Path_fr('does_not_exist'))
-        self.assertEqual('str Path_fr', type_to_str(Path_fr))
 
 
     def test_already_registered(self):
@@ -175,25 +173,10 @@ class OtherTests(unittest.TestCase):
         self.assertRaises(ValueError, lambda: restricted_string_type('List', '^clash$'))
 
 
-    def test_is_optional(self):
-        class MyEnum(Enum):
-            A = 1
-
-        params = [
-            (Optional[bool],             bool, True),
-            (Union[type(None), bool],    bool, True),
-            (Dict[bool, type(None)],     bool, False),
-            (Optional[Path_fr],          Path, True),
-            (Union[type(None), Path_fr], Path, True),
-            (Dict[Path_fr, type(None)],  Path, False),
-            (Optional[MyEnum],           Enum, True),
-            (Union[type(None), MyEnum],  Enum, True),
-            (Dict[MyEnum, type(None)],   Enum, False),
-        ]
-
-        for annotation, ref_type, expected in params:
-            with self.subTest(str(annotation)):
-                self.assertEqual(expected, is_optional(annotation, ref_type))
+    def test_Callable(self):
+        CallableType = registered_types[Callable]
+        self.assertEqual(CallableType.deserializer('jsonargparse.CLI'), CLI)
+        self.assertRaises(ValueError, lambda: CallableType.deserializer(None))
 
 
 if __name__ == '__main__':