diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_cross_modulestore_import_export.py b/common/lib/xmodule/xmodule/modulestore/tests/test_cross_modulestore_import_export.py
index 7ebef5e4d51df46157209ebdd35f654a786debcb..042198113fbceb510a351167db6644a4a282114e 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_cross_modulestore_import_export.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_cross_modulestore_import_export.py
@@ -121,6 +121,9 @@ class CrossStoreXMLRoundtrip(CourseComparisonTest, PartitionTestCase):
                         self.exclude_field(None, 'wiki_slug')
                         self.exclude_field(None, 'xml_attributes')
                         self.exclude_field(None, 'parent')
+                        # discussion_ids are auto-generated based on usage_id, so they should change across
+                        # modulestores - see TNL-5001
+                        self.exclude_field(None, 'discussion_id')
                         self.ignore_asset_key('_id')
                         self.ignore_asset_key('uploadDate')
                         self.ignore_asset_key('content_son')
diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py
index eacfb2037f803faede4ef8e63d7261266509c3c0..fa70cc36344b03b7ed431dd7ef582c4b841ed806 100644
--- a/common/lib/xmodule/xmodule/xml_module.py
+++ b/common/lib/xmodule/xmodule/xml_module.py
@@ -335,7 +335,7 @@ class XmlParserMixin(object):
 
         """
         # VS[compat] -- just have the url_name lookup, once translation is done
-        url_name = node.get('url_name', node.get('slug'))
+        url_name = cls._get_url_name(node)
         def_id = id_generator.create_definition(node.tag, url_name)
         usage_id = id_generator.create_usage(def_id)
         aside_children = []
@@ -344,8 +344,7 @@ class XmlParserMixin(object):
         if is_pointer_tag(node):
             # new style:
             # read the actual definition file--named using url_name.replace(':','/')
-            filepath = cls._format_filepath(node.tag, name_to_pathname(url_name))
-            definition_xml = cls.load_file(filepath, runtime.resources_fs, def_id)
+            definition_xml, filepath = cls.load_definition_xml(node, runtime, def_id)
             aside_children = runtime.parse_asides(definition_xml, def_id, usage_id, id_generator)
         else:
             filepath = None
@@ -408,6 +407,23 @@ class XmlParserMixin(object):
 
         return xblock
 
+    @classmethod
+    def _get_url_name(cls, node):
+        """
+        Reads url_name attribute from the node
+        """
+        return node.get('url_name', node.get('slug'))
+
+    @classmethod
+    def load_definition_xml(cls, node, runtime, def_id):
+        """
+        Loads definition_xml stored in a dedicated file
+        """
+        url_name = cls._get_url_name(node)
+        filepath = cls._format_filepath(node.tag, name_to_pathname(url_name))
+        definition_xml = cls.load_file(filepath, runtime.resources_fs, def_id)
+        return definition_xml, filepath
+
     @classmethod
     def _format_filepath(cls, category, name):
         return u'{category}/{name}.{ext}'.format(category=category,
diff --git a/openedx/core/lib/xblock_builtin/xblock_discussion/tests.py b/openedx/core/lib/xblock_builtin/xblock_discussion/tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..b02e08904129f40045b5c53ff60519df39ae7652
--- /dev/null
+++ b/openedx/core/lib/xblock_builtin/xblock_discussion/tests.py
@@ -0,0 +1,181 @@
+""" Tests for DiscussionXBLock"""
+from collections import namedtuple
+import ddt
+import itertools
+import mock
+from nose.plugins.attrib import attr
+import random
+import string
+from unittest import TestCase
+
+from safe_lxml import etree
+
+from openedx.core.lib.xblock_builtin.xblock_discussion.xblock_discussion import DiscussionXBlock
+from xblock.field_data import DictFieldData
+from xblock.fields import ScopeIds, UNIQUE_ID, NO_CACHE_VALUE
+from xblock.runtime import Runtime
+
+
+AttributePair = namedtuple("AttributePair", ["name", "value"])
+
+
+ID_ATTR_NAMES = ("discussion_id", "id",)
+CATEGORY_ATTR_NAMES = ("discussion_category",)
+TARGET_ATTR_NAMES = ("discussion_target", "for", )
+
+
+def _random_string():
+    """
+    Generates random string
+    """
+    return ''.join(random.choice(string.lowercase, ) for _ in xrange(12))
+
+
+def _make_attribute_test_cases():
+    """
+    Builds test cases for attribute-dependent tests
+    """
+    attribute_names = itertools.product(ID_ATTR_NAMES, CATEGORY_ATTR_NAMES, TARGET_ATTR_NAMES)
+    for id_attr, category_attr, target_attr in attribute_names:
+        yield (
+            AttributePair(id_attr, _random_string()),
+            AttributePair(category_attr, _random_string()),
+            AttributePair(target_attr, _random_string())
+        )
+
+
+@attr('shard2')
+@ddt.ddt
+class DiscussionXBlockImportExportTests(TestCase):
+    """
+    Import and export tests
+    """
+    DISCUSSION_XBLOCK_LOCATION = "openedx.core.lib.xblock_builtin.xblock_discussion.xblock_discussion.DiscussionXBlock"
+
+    def setUp(self):
+        """
+        Set up method
+        """
+        super(DiscussionXBlockImportExportTests, self).setUp()
+        self.keys = ScopeIds("any_user", "discussion", "def_id", "usage_id")
+        self.runtime_mock = mock.Mock(spec=Runtime)
+        self.runtime_mock.construct_xblock_from_class = mock.Mock(side_effect=self._construct_xblock_mock)
+        self.runtime_mock.get_policy = mock.Mock(return_value={})
+        self.id_gen_mock = mock.Mock()
+
+    def _construct_xblock_mock(self, cls, keys):  # pylint: disable=unused-argument
+        """
+        Builds target xblock instance (DiscussionXBlock)
+
+        Signature-compatible with runtime.construct_xblock_from_class - can be used as a mock side-effect
+        """
+        return DiscussionXBlock(self.runtime_mock, scope_ids=keys, field_data=DictFieldData({}))
+
+    @mock.patch(DISCUSSION_XBLOCK_LOCATION + ".load_definition_xml")
+    @ddt.unpack
+    @ddt.data(*list(_make_attribute_test_cases()))
+    def test_xblock_export_format(self, id_pair, category_pair, target_pair, patched_load_definition_xml):
+        """
+        Test that xblock export XML format can be parsed preserving field values
+        """
+        xblock_xml = """
+        <discussion
+            url_name="82bb87a2d22240b1adac2dfcc1e7e5e4" xblock-family="xblock.v1"
+            {id_attr}="{id_value}"
+            {category_attr}="{category_value}"
+            {target_attr}="{target_value}"
+        />
+        """.format(
+            id_attr=id_pair.name, id_value=id_pair.value,
+            category_attr=category_pair.name, category_value=category_pair.value,
+            target_attr=target_pair.name, target_value=target_pair.value,
+        )
+        node = etree.fromstring(xblock_xml)
+
+        patched_load_definition_xml.side_effect = Exception("Irrelevant")
+
+        block = DiscussionXBlock.parse_xml(node, self.runtime_mock, self.keys, self.id_gen_mock)
+        try:
+            self.assertEqual(block.discussion_id, id_pair.value)
+            self.assertEqual(block.discussion_category, category_pair.value)
+            self.assertEqual(block.discussion_target, target_pair.value)
+        except AssertionError:
+            print xblock_xml
+            raise
+
+    @mock.patch(DISCUSSION_XBLOCK_LOCATION + ".load_definition_xml")
+    @ddt.unpack
+    @ddt.data(*(_make_attribute_test_cases()))
+    def test_legacy_export_format(self, id_pair, category_pair, target_pair, patched_load_definition_xml):
+        """
+        Test that legacy export XML format can be parsed preserving field values
+        """
+        xblock_xml = """<discussion url_name="82bb87a2d22240b1adac2dfcc1e7e5e4"/>"""
+        xblock_definition_xml = """
+        <discussion
+            {id_attr}="{id_value}"
+            {category_attr}="{category_value}"
+            {target_attr}="{target_value}"
+        />""".format(
+            id_attr=id_pair.name, id_value=id_pair.value,
+            category_attr=category_pair.name, category_value=category_pair.value,
+            target_attr=target_pair.name, target_value=target_pair.value,
+        )
+        node = etree.fromstring(xblock_xml)
+        definition_node = etree.fromstring(xblock_definition_xml)
+
+        patched_load_definition_xml.return_value = (definition_node, "irrelevant")
+
+        block = DiscussionXBlock.parse_xml(node, self.runtime_mock, self.keys, self.id_gen_mock)
+        try:
+            self.assertEqual(block.discussion_id, id_pair.value)
+            self.assertEqual(block.discussion_category, category_pair.value)
+            self.assertEqual(block.discussion_target, target_pair.value)
+        except AssertionError:
+            print xblock_xml, xblock_definition_xml
+            raise
+
+    def test_export_default_discussion_id(self):
+        """
+        Test that default discussion_id values are not exported.
+
+        Historically, the OLX format allowed omitting discussion ID values; in such case, the IDs are generated
+        deterministically based on the course ID and the usage ID. Moreover, Studio does not allow course authors
+        to edit discussion_id, so all courses authored in Studio have discussion_id omitted in OLX.
+
+        Forcing Studio to always export discussion_id can cause data loss when switching between an older and newer
+        export,  in a course with a course ID different from the one from which the export was created - because the
+        discussion ID would be different.
+        """
+        target_node = etree.Element('dummy')
+
+        block = DiscussionXBlock(self.runtime_mock, scope_ids=self.keys, field_data=DictFieldData({}))
+        discussion_id_field = block.fields['discussion_id']
+
+        # precondition checks - discussion_id does not have a value and uses UNIQUE_ID
+        self.assertEqual(
+            discussion_id_field._get_cached_value(block),  # pylint: disable=protected-access
+            NO_CACHE_VALUE
+        )
+        self.assertEqual(discussion_id_field.default, UNIQUE_ID)
+
+        block.add_xml_to_node(target_node)
+        self.assertEqual(target_node.tag, "discussion")
+        self.assertNotIn("discussion_id", target_node.attrib)
+
+    @ddt.data("jediwannabe", "iddqd", "itisagooddaytodie")
+    def test_export_custom_discussion_id(self, discussion_id):
+        """
+        Test that custom discussion_id values are exported
+        """
+        target_node = etree.Element('dummy')
+
+        block = DiscussionXBlock(self.runtime_mock, scope_ids=self.keys, field_data=DictFieldData({}))
+        block.discussion_id = discussion_id
+
+        # precondition check
+        self.assertEqual(block.discussion_id, discussion_id)
+
+        block.add_xml_to_node(target_node)
+        self.assertEqual(target_node.tag, "discussion")
+        self.assertTrue(target_node.attrib["discussion_id"], discussion_id)
diff --git a/openedx/core/lib/xblock_builtin/xblock_discussion/xblock_discussion.py b/openedx/core/lib/xblock_builtin/xblock_discussion/xblock_discussion.py
index 50e1c578401a043a795ab1ec1d09425cec68d7ac..0f55f48bd81ee11646d94962e35f1113aa6bde88 100644
--- a/openedx/core/lib/xblock_builtin/xblock_discussion/xblock_discussion.py
+++ b/openedx/core/lib/xblock_builtin/xblock_discussion/xblock_discussion.py
@@ -6,10 +6,12 @@ import logging
 
 from xblockutils.resources import ResourceLoader
 from xblockutils.studio_editable import StudioEditableXBlockMixin
+from xmodule.raw_module import RawDescriptor
 
 from xblock.core import XBlock
 from xblock.fields import Scope, String, UNIQUE_ID
 from xblock.fragment import Fragment
+from xmodule.xml_module import XmlParserMixin
 
 log = logging.getLogger(__name__)
 loader = ResourceLoader(__name__)  # pylint: disable=invalid-name
@@ -24,11 +26,11 @@ def _(text):
 
 @XBlock.needs('user')
 @XBlock.needs('i18n')
-class DiscussionXBlock(XBlock, StudioEditableXBlockMixin):
+class DiscussionXBlock(XBlock, StudioEditableXBlockMixin, XmlParserMixin):
     """
     Provides a discussion forum that is inline with other content in the courseware.
     """
-    discussion_id = String(scope=Scope.settings, default=UNIQUE_ID, force_export=True)
+    discussion_id = String(scope=Scope.settings, default=UNIQUE_ID)
     display_name = String(
         display_name=_("Display Name"),
         help=_("Display name for this component"),
@@ -59,6 +61,11 @@ class DiscussionXBlock(XBlock, StudioEditableXBlockMixin):
 
     has_author_view = True  # Tells Studio to use author_view
 
+    # support for legacy OLX format - consumed by XmlParserMixin.load_metadata
+    metadata_translations = dict(RawDescriptor.metadata_translations)
+    metadata_translations['id'] = 'discussion_id'
+    metadata_translations['for'] = 'discussion_target'
+
     @property
     def course_key(self):
         """
@@ -134,3 +141,55 @@ class DiscussionXBlock(XBlock, StudioEditableXBlockMixin):
         Returns a JSON representation of the student_view of this XBlock.
         """
         return {'topic_id': self.discussion_id}
+
+    @classmethod
+    def parse_xml(cls, node, runtime, keys, id_generator):
+        """
+        Parses OLX into XBlock.
+
+        This method is overridden here to allow parsing legacy OLX, coming from discussion XModule.
+        XBlock stores all the associated data, fields and children in a XML element inlined into vertical XML file
+        XModule stored only minimal data on the element included into vertical XML and used a dedicated "discussion"
+        folder in OLX to store fields and children. Also, some info was put into "policy.json" file.
+
+        If no external data sources are found (file in "discussion" folder), it is exactly equivalent to base method
+        XBlock.parse_xml. Otherwise this method parses file in "discussion" folder (known as definition_xml), applies
+        policy.json and updates fields accordingly.
+        """
+        block = super(DiscussionXBlock, cls).parse_xml(node, runtime, keys, id_generator)
+
+        cls._apply_translations_to_node_attributes(block, node)
+        cls._apply_metadata_and_policy(block, node, runtime)
+
+        return block
+
+    @classmethod
+    def _apply_translations_to_node_attributes(cls, block, node):
+        """
+        Applies metadata translations for attributes stored on an inlined XML element.
+        """
+        for old_attr, target_attr in cls.metadata_translations.iteritems():
+            if old_attr in node.attrib and hasattr(block, target_attr):
+                setattr(block, target_attr, node.attrib[old_attr])
+
+    @classmethod
+    def _apply_metadata_and_policy(cls, block, node, runtime):
+        """
+        Attempt to load definition XML from "discussion" folder in OLX, than parse it and update block fields
+        """
+        try:
+            definition_xml, _ = cls.load_definition_xml(node, runtime, block.scope_ids.def_id)
+        except Exception as err:  # pylint: disable=broad-except
+            log.info(
+                "Exception %s when trying to load definition xml for block %s - assuming XBlock export format",
+                err,
+                block
+            )
+            return
+
+        metadata = cls.load_metadata(definition_xml)
+        cls.apply_policy(metadata, runtime.get_policy(block.scope_ids.usage_id))
+
+        for field_name, value in metadata.iteritems():
+            if field_name in block.fields:
+                setattr(block, field_name, value)