diff --git a/designateclient/client.py b/designateclient/client.py
index b4df6e6b..0c8c9124 100644
--- a/designateclient/client.py
+++ b/designateclient/client.py
@@ -44,7 +44,10 @@ class Controller(object):
         }
 
     def _serialize(self, kwargs):
-        if 'data' in kwargs:
+        headers = kwargs.get('headers')
+        content_type = headers.get('Content-Type') if headers else None
+
+        if 'data' in kwargs and content_type in {None, 'application/json'}:
             kwargs['data'] = json.dumps(kwargs['data'])
 
     def _post(self, url, response_key=None, **kwargs):
diff --git a/designateclient/functionaltests/base.py b/designateclient/functionaltests/base.py
index 5d6835c1..faa0fcb0 100644
--- a/designateclient/functionaltests/base.py
+++ b/designateclient/functionaltests/base.py
@@ -31,3 +31,16 @@ class BaseDesignateTest(base.ClientTestBase):
             self.clients.as_user('admin').tld_create(tld)
         except CommandFailed:
             pass
+
+    def _is_entity_in_list(self, entity, entity_list):
+        """Determines if the given entity exists in the given list.
+
+        Uses the id for comparison.
+
+        Certain entities (e.g. zone import, export) cannot be made
+        comparable in a list of CLI output results, because the fields
+        in a list command can be different from those in a show command.
+
+        """
+        return any([entity_record.id == entity.id
+                    for entity_record in entity_list])
diff --git a/designateclient/functionaltests/client.py b/designateclient/functionaltests/client.py
index 370d48f5..5aefacc6 100644
--- a/designateclient/functionaltests/client.py
+++ b/designateclient/functionaltests/client.py
@@ -173,6 +173,26 @@ class ZoneExportCommands(object):
         return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
 
 
+class ZoneImportCommands(object):
+    """A mixin for DesignateCLI to add zone import commands"""
+
+    def zone_import_list(self, *args, **kwargs):
+        cmd = 'zone import list'
+        return self.parsed_cmd(cmd, ListModel, *args, **kwargs)
+
+    def zone_import_create(self, zone_file_path, *args, **kwargs):
+        cmd = 'zone import create {0}'.format(zone_file_path)
+        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
+
+    def zone_import_show(self, zone_import_id, *args, **kwargs):
+        cmd = 'zone import show {0}'.format(zone_import_id)
+        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
+
+    def zone_import_delete(self, zone_import_id, *args, **kwargs):
+        cmd = 'zone import delete {0}'.format(zone_import_id)
+        return self.parsed_cmd(cmd, *args, **kwargs)
+
+
 class RecordsetCommands(object):
 
     def recordset_show(self, zone_id, id, *args, **kwargs):
@@ -283,8 +303,8 @@ class BlacklistCommands(object):
 
 
 class DesignateCLI(base.CLIClient, ZoneCommands, ZoneTransferCommands,
-                   ZoneExportCommands, RecordsetCommands, TLDCommands,
-                   BlacklistCommands):
+                   ZoneExportCommands, ZoneImportCommands, RecordsetCommands,
+                   TLDCommands, BlacklistCommands):
 
     # instantiate this once to minimize requests to keystone
     _CLIENTS = None
diff --git a/designateclient/functionaltests/datagen.py b/designateclient/functionaltests/datagen.py
index 2f893215..58f952c9 100644
--- a/designateclient/functionaltests/datagen.py
+++ b/designateclient/functionaltests/datagen.py
@@ -35,3 +35,13 @@ def random_a_recordset_name(zone_name, recordset_name='testrecord'):
 
 def random_blacklist(name='testblacklist'):
     return '{0}{1}'.format(name, random_digits())
+
+
+def random_zone_file(name='testzoneimport'):
+    return "$ORIGIN {0}{1}.com.\n" \
+           "$TTL 300\n" \
+           "{0}{1}.com. 300 IN SOA ns.{0}{1}.com. nsadmin.{0}{1}.com. 42 42 42 42 42\n" \
+           "{0}{1}.com. 300 IN NS ns.{0}{1}.com.\n" \
+           "{0}{1}.com. 300 IN MX 10 mail.{0}{1}.com.\n" \
+           "ns.{0}{1}.com. 300 IN A 10.0.0.1\n" \
+           "mail.{0}{1}.com. 300 IN A 10.0.0.2\n".format(name, random_digits())
diff --git a/designateclient/functionaltests/v2/fixtures.py b/designateclient/functionaltests/v2/fixtures.py
index 2e61148c..75f4406e 100644
--- a/designateclient/functionaltests/v2/fixtures.py
+++ b/designateclient/functionaltests/v2/fixtures.py
@@ -16,6 +16,7 @@ limitations under the License.
 from __future__ import absolute_import
 from __future__ import print_function
 import sys
+import tempfile
 import traceback
 
 import fixtures
@@ -123,6 +124,38 @@ class ExportFixture(BaseFixture):
             pass
 
 
+class ImportFixture(BaseFixture):
+    """See DesignateCLI.zone_import_create for __init__ args"""
+
+    def __init__(self, zone_file_contents, user='default', *args, **kwargs):
+        super(ImportFixture, self).__init__(user, *args, **kwargs)
+        self.zone_file_contents = zone_file_contents
+
+    def _setUp(self):
+        super(ImportFixture, self)._setUp()
+
+        with tempfile.NamedTemporaryFile() as f:
+            f.write(self.zone_file_contents)
+            f.flush()
+
+            self.zone_import = self.client.zone_import_create(
+                zone_file_path=f.name,
+                *self.args, **self.kwargs
+            )
+
+        self.addCleanup(self.cleanup_zone_import, self.client,
+                        self.zone_import.id)
+        self.addCleanup(ZoneFixture.cleanup_zone, self.client,
+                        self.zone_import.zone_id)
+
+    @classmethod
+    def cleanup_zone_import(cls, client, zone_import_id):
+        try:
+            client.zone_import_delete(zone_import_id)
+        except CommandFailed:
+            pass
+
+
 class RecordsetFixture(BaseFixture):
     """See DesignateCLI.recordset_create for __init__ args"""
 
diff --git a/designateclient/functionaltests/v2/test_zone_export.py b/designateclient/functionaltests/v2/test_zone_export.py
index fc10f851..6052a997 100644
--- a/designateclient/functionaltests/v2/test_zone_export.py
+++ b/designateclient/functionaltests/v2/test_zone_export.py
@@ -39,7 +39,7 @@ class TestZoneExport(BaseDesignateTest):
 
         zone_exports = self.clients.zone_export_list()
         self.assertGreater(len(zone_exports), 0)
-        self.assertTrue(self._is_export_in_list(zone_export, zone_exports))
+        self.assertTrue(self._is_entity_in_list(zone_export, zone_exports))
 
     def test_create_and_show_zone_export(self):
         zone_export = self.useFixture(ExportFixture(
@@ -60,12 +60,12 @@ class TestZoneExport(BaseDesignateTest):
         )).zone_export
 
         zone_exports = self.clients.zone_export_list()
-        self.assertTrue(self._is_export_in_list(zone_export, zone_exports))
+        self.assertTrue(self._is_entity_in_list(zone_export, zone_exports))
 
         self.clients.zone_export_delete(zone_export.id)
 
         zone_exports = self.clients.zone_export_list()
-        self.assertFalse(self._is_export_in_list(zone_export, zone_exports))
+        self.assertFalse(self._is_entity_in_list(zone_export, zone_exports))
 
     def test_show_export_file(self):
         zone_export = self.useFixture(ExportFixture(
@@ -79,16 +79,3 @@ class TestZoneExport(BaseDesignateTest):
         self.assertIn('SOA', fetched_export.data)
         self.assertIn('NS', fetched_export.data)
         self.assertIn(self.zone.name, fetched_export.data)
-
-    def _is_export_in_list(self, zone_export, zone_export_list):
-        """Determines if the given export exists in the given export list.
-
-        Uses the zone export id for comparison.
-
-        Because the zone export list command displays fewer fields than
-        the show command, an __eq__ method on the FieldValueModel class
-        is insufficient.
-
-        """
-        return any([export_record.id == zone_export.id
-                    for export_record in zone_export_list])
diff --git a/designateclient/functionaltests/v2/test_zone_import.py b/designateclient/functionaltests/v2/test_zone_import.py
new file mode 100644
index 00000000..232b57f2
--- /dev/null
+++ b/designateclient/functionaltests/v2/test_zone_import.py
@@ -0,0 +1,63 @@
+"""
+Copyright 2016 Rackspace
+
+Author: Rahman Syed <rahman.syed@gmail.com>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+"""
+from designateclient.functionaltests.base import BaseDesignateTest
+from designateclient.functionaltests.datagen import random_zone_file
+from designateclient.functionaltests.v2.fixtures import ImportFixture
+
+
+class TestZoneImport(BaseDesignateTest):
+
+    def setUp(self):
+        super(TestZoneImport, self).setUp()
+        self.ensure_tld_exists('com')
+        self.zone_file_contents = random_zone_file()
+
+    def test_list_zone_imports(self):
+        zone_import = self.useFixture(ImportFixture(
+            zone_file_contents=self.zone_file_contents
+        )).zone_import
+
+        zone_imports = self.clients.zone_import_list()
+        self.assertGreater(len(zone_imports), 0)
+        self.assertTrue(self._is_entity_in_list(zone_import, zone_imports))
+
+    def test_create_and_show_zone_import(self):
+        zone_import = self.useFixture(ImportFixture(
+            zone_file_contents=self.zone_file_contents
+        )).zone_import
+
+        fetched_import = self.clients.zone_import_show(zone_import.id)
+
+        self.assertEqual(zone_import.created_at, fetched_import.created_at)
+        self.assertEqual(zone_import.id, fetched_import.id)
+        self.assertEqual(zone_import.project_id, fetched_import.project_id)
+
+        self.assertEqual('COMPLETE', fetched_import.status)
+
+    def test_delete_zone_import(self):
+        zone_import = self.useFixture(ImportFixture(
+            zone_file_contents=self.zone_file_contents
+        )).zone_import
+
+        zone_imports = self.clients.zone_import_list()
+        self.assertTrue(self._is_entity_in_list(zone_import, zone_imports))
+
+        self.clients.zone_import_delete(zone_import.id)
+
+        zone_imports = self.clients.zone_import_list()
+        self.assertFalse(self._is_entity_in_list(zone_import, zone_imports))
diff --git a/designateclient/tests/v2/test_zones.py b/designateclient/tests/v2/test_zones.py
index 673615ee..27239921 100644
--- a/designateclient/tests/v2/test_zones.py
+++ b/designateclient/tests/v2/test_zones.py
@@ -312,3 +312,57 @@ class TestZoneExports(v2.APIV2TestCase, v2.CrudMixin):
 
         response = self.client.zone_exports.get_export(ref["id"])
         self.assertEqual(ref, response)
+
+
+class TestZoneImports(v2.APIV2TestCase, v2.CrudMixin):
+    def new_ref(self, **kwargs):
+        ref = super(TestZoneImports, self).new_ref(**kwargs)
+        ref.setdefault("zone_id", uuid.uuid4().hex)
+        ref.setdefault("created_at", time.strftime("%c"))
+        ref.setdefault("updated_at", time.strftime("%c"))
+        ref.setdefault("status", 'PENDING')
+        ref.setdefault("message", 'Importing...')
+        ref.setdefault("version", '1')
+        return ref
+
+    def test_create_import(self):
+        zonefile = '$ORIGIN example.com'
+
+        parts = ["zones", "tasks", "imports"]
+        self.stub_url('POST', parts=parts, json=zonefile)
+
+        self.client.zone_imports.create(zonefile)
+        self.assertRequestBodyIs(body=zonefile)
+
+    def test_get_import(self):
+        ref = self.new_ref()
+
+        parts = ["zones", "tasks", "imports", ref["id"]]
+        self.stub_url('GET', parts=parts, json=ref)
+        self.stub_entity("GET", parts=parts, entity=ref, id=ref["id"])
+
+        response = self.client.zone_imports.get_import_record(ref["id"])
+        self.assertEqual(ref, response)
+
+    def test_list_imports(self):
+        items = [
+            self.new_ref(),
+            self.new_ref()
+        ]
+
+        parts = ["zones", "tasks", "imports"]
+        self.stub_url('GET', parts=parts, json={"imports": items})
+
+        listed = self.client.zone_imports.list()
+        self.assertList(items, listed["imports"])
+        self.assertQueryStringIs("")
+
+    def test_delete_import(self):
+        ref = self.new_ref()
+
+        parts = ["zones", "tasks", "imports", ref["id"]]
+        self.stub_url('DELETE', parts=parts, json=ref)
+        self.stub_entity("DELETE", parts=parts, id=ref["id"])
+
+        self.client.zone_imports.delete(ref["id"])
+        self.assertRequestBodyIs(None)
diff --git a/designateclient/v2/cli/zones.py b/designateclient/v2/cli/zones.py
index 938a49af..09a96b2c 100644
--- a/designateclient/v2/cli/zones.py
+++ b/designateclient/v2/cli/zones.py
@@ -37,6 +37,10 @@ def _format_zone_export_record(zone_export_record):
     zone_export_record.pop('links', None)
 
 
+def _format_zone_import_record(zone_import_record):
+    zone_import_record.pop('links', None)
+
+
 class ListZonesCommand(lister.Lister):
     """List zones"""
 
@@ -498,3 +502,96 @@ class ShowZoneExportFileCommand(show.ShowOne):
         data = client.zone_exports.get_export(parsed_args.zone_export_id)
 
         return ['data'], [data]
+
+
+class ImportZoneCommand(show.ShowOne):
+    """Import a Zone from a file on the filesystem"""
+
+    def get_parser(self, prog_name):
+        parser = super(ImportZoneCommand, self).get_parser(
+            prog_name)
+
+        parser.add_argument('zone_file_path',
+                            help="Path to a zone file", type=str)
+
+        return parser
+
+    def take_action(self, parsed_args):
+        client = self.app.client_manager.dns
+
+        with open(parsed_args.zone_file_path, 'r') as f:
+            zone_file_contents = f.read()
+
+        data = client.zone_imports.create(zone_file_contents)
+        _format_zone_import_record(data)
+
+        LOG.info('Zone Import %s was created', data['id'])
+
+        return six.moves.zip(*sorted(six.iteritems(data)))
+
+
+class ListZoneImportsCommand(lister.Lister):
+    """List Zone Imports"""
+
+    columns = [
+        'id',
+        'zone_id',
+        'created_at',
+        'status',
+        'message',
+    ]
+
+    def get_parser(self, prog_name):
+        parser = super(ListZoneImportsCommand, self).get_parser(
+            prog_name)
+
+        return parser
+
+    def take_action(self, parsed_args):
+        client = self.app.client_manager.dns
+
+        data = client.zone_imports.list()
+
+        cols = self.columns
+        return cols, (utils.get_item_properties(s, cols)
+                      for s in data['imports'])
+
+
+class ShowZoneImportCommand(show.ShowOne):
+    """Show a Zone Import"""
+
+    def get_parser(self, prog_name):
+        parser = super(ShowZoneImportCommand, self).get_parser(
+            prog_name)
+
+        parser.add_argument('zone_import_id', help="Zone Import ID", type=str)
+
+        return parser
+
+    def take_action(self, parsed_args):
+        client = self.app.client_manager.dns
+
+        data = client.zone_imports.get_import_record(
+            parsed_args.zone_import_id)
+        _format_zone_import_record(data)
+
+        return six.moves.zip(*sorted(six.iteritems(data)))
+
+
+class DeleteZoneImportCommand(command.Command):
+    """Delete a Zone Import"""
+
+    def get_parser(self, prog_name):
+        parser = super(DeleteZoneImportCommand, self).get_parser(
+            prog_name)
+
+        parser.add_argument('zone_import_id', help="Zone Import ID", type=str)
+
+        return parser
+
+    def take_action(self, parsed_args):
+        client = self.app.client_manager.dns
+
+        client.zone_imports.delete(parsed_args.zone_import_id)
+
+        LOG.info('Zone Import %s was deleted', parsed_args.zone_import_id)
diff --git a/designateclient/v2/zones.py b/designateclient/v2/zones.py
index 8c200c8d..fa9abad0 100644
--- a/designateclient/v2/zones.py
+++ b/designateclient/v2/zones.py
@@ -147,4 +147,15 @@ class ZoneExportsController(V2Controller):
 
 
 class ZoneImportsController(V2Controller):
-    pass
+    def create(self, zone_file_contents):
+        return self._post('/zones/tasks/imports', data=zone_file_contents,
+                          headers={'Content-Type': 'text/dns'})
+
+    def get_import_record(self, zone_import_id):
+        return self._get('/zones/tasks/imports/%s' % zone_import_id)
+
+    def list(self):
+        return self._get('/zones/tasks/imports')
+
+    def delete(self, zone_import_id):
+        return self._delete('/zones/tasks/imports/%s' % zone_import_id)
diff --git a/setup.cfg b/setup.cfg
index 1c9751d7..df44212b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -115,6 +115,11 @@ openstack.dns.v2 =
     zone_export_delete = designateclient.v2.cli.zones:DeleteZoneExportCommand
     zone_export_showfile = designateclient.v2.cli.zones:ShowZoneExportFileCommand
 
+    zone_import_create = designateclient.v2.cli.zones:ImportZoneCommand
+    zone_import_list = designateclient.v2.cli.zones:ListZoneImportsCommand
+    zone_import_show = designateclient.v2.cli.zones:ShowZoneImportCommand
+    zone_import_delete = designateclient.v2.cli.zones:DeleteZoneImportCommand
+
     zone_transfer_request_create = designateclient.v2.cli.zones:CreateTransferRequestCommand
     zone_transfer_request_list = designateclient.v2.cli.zones:ListTransferRequestsCommand
     zone_transfer_request_show = designateclient.v2.cli.zones:ShowTransferRequestCommand