diff --git a/reviewboard/reviews/tests/test_json_review_ui.py b/reviewboard/reviews/tests/test_json_review_ui.py
new file mode 100644
index 0000000000000000000000000000000000000000..1d9d611f30fd76d31eeaf83e4f1ecba0cc4c51ab
--- /dev/null
+++ b/reviewboard/reviews/tests/test_json_review_ui.py
@@ -0,0 +1,131 @@
+"""Unit tests for reviewboard.reviews.ui.text.JsonReviewUI."""
+from __future__ import unicode_literals
+
+from django.test.client import RequestFactory
+
+from reviewboard.reviews.ui.jsonui import (
+    JsonReviewUI,
+    render_json)
+import json
+from reviewboard.testing import TestCase
+from collections import OrderedDict
+
+
+class JsonReviewUITests(TestCase):
+    """Unit tests for reviewboard.reviews.ui.text.JsonReviewUI."""
+
+    fixtures = ['test_users', 'test_scmtools']
+
+    def setUp(self):
+        super(JsonReviewUITests, self).setUp()
+
+        self.review_request = self.create_review_request()
+        self.attachment_history = self.create_file_attachment_history(
+            self.review_request)
+        self.attachment = self.create_file_attachment(
+            self.review_request,
+            attachment_history=self.attachment_history,
+            attachment_revision=0,
+            caption='Revision 1 caption',
+            orig_filename='revision1.json',
+            mimetype='application/json',
+            file_content=b'{"revision": "one, not two"}')
+        self.review = self.create_review(self.review_request)
+
+    def test_get_extra_context_with_diff(self):
+        """Testing JSONReviewUI.get_extra_context with diff_against_obj"""
+        new_attachment = self.create_file_attachment(
+            self.review_request,
+            attachment_history=self.attachment_history,
+            attachment_revision=1,
+            caption='Revision 2 caption',
+            orig_filename='revision2.json',
+            mimetype='application/json',
+            file_content=b'{"revision": "Two"}')
+
+        review_ui = JsonReviewUI(review_request=self.review_request,
+                                 obj=new_attachment)
+        review_ui.set_diff_against(self.attachment)
+
+        request = RequestFactory().get('/')
+        extra_context = review_ui.get_extra_context(request)
+
+        self.assertTrue(extra_context['is_diff'])
+        self.assertFalse(extra_context['diff_type_mismatch'])
+        self.assertEqual(extra_context['filename'], 'revision2.json')
+        self.assertEqual(extra_context['revision'], 1)
+        self.assertEqual(extra_context['num_revisions'], 2)
+        self.assertEqual(extra_context['diff_caption'], 'Revision 1 caption')
+        self.assertEqual(extra_context['diff_filename'], 'revision1.json')
+        self.assertEqual(extra_context['diff_revision'], 0)
+        self.assertEqual(
+            list(extra_context['source_chunks']),
+            [
+                {
+                    'change': 'replace',
+                    'collapsable': False,
+                    'index': 0,
+                    'lines': [
+                        [
+                            1,
+                            1,
+                            '<span class="p">{</span>'
+                            '<span class="nt">&quot;revision&quot;</span>'
+                            '<span class="p">:</span> '
+                            '<span class="s2">&quot;one, not two&quot;</span>'
+                            '<span class="p">}</span>',
+                            [(14, 24)],
+                            1,
+                            '<span class="p">{</span>'
+                            '<span class="nt">&quot;revision&quot;</span>'
+                            '<span class="p">:</span> '
+                            '<span class="s2">&quot;Two&quot;</span>'
+                            '<span class="p">}</span>',
+                            [(14, 15)],
+                            False,
+                        ],
+                    ],
+                    'meta': {
+                        'left_headers': [],
+                        'right_headers': [],
+                        'whitespace_chunk': False,
+                        'whitespace_lines': [],
+                    },
+                    'numlines': 1,
+                },
+            ])
+        self.assertNotIn('text_lines', extra_context)
+        self.assertNotIn('rendered_lines', extra_context)
+
+    def test_render_json_sorted_keys(self):
+        """Testing render_json when sorted_keys is True"""
+        input_text = json.loads('{"z_test_key": "y", "a_test_key": "b"}',
+                                object_pairs_hook=OrderedDict)
+        print(input_text)
+        output = render_json(input_text, True)
+        expected = ['<span class="p">{</span>',
+                    '    <span class="nt">&quot;a_test_key&quot;</span>'
+                    '<span class="p">:</span> '
+                    '<span class="s2">&quot;b&quot;</span>'
+                    '<span class="p">,</span> ',
+                    '    <span class="nt">&quot;z_test_key&quot;</span>'
+                    '<span class="p">:</span> '
+                    '<span class="s2">&quot;y&quot;</span>',
+                    '<span class="p">}</span>']
+        self.assertEqual(output, expected)
+
+    def test_render_json_unsorted_keys(self):
+        """Testing render_json when sorted_keys is False"""
+        input_text = json.loads('{"z_test_key": "y", "a_test_key": "b"}',
+                                object_pairs_hook=OrderedDict)
+        output = render_json(input_text, False)
+        expected = ['<span class="p">{</span>',
+                    '    <span class="nt">&quot;z_test_key&quot;</span>'
+                    '<span class="p">:</span> '
+                    '<span class="s2">&quot;y&quot;</span>'
+                    '<span class="p">,</span> ',
+                    '    <span class="nt">&quot;a_test_key&quot;</span>'
+                    '<span class="p">:</span> '
+                    '<span class="s2">&quot;b&quot;</span>',
+                    '<span class="p">}</span>']
+        self.assertEqual(output, expected)
diff --git a/reviewboard/reviews/ui/__init__.py b/reviewboard/reviews/ui/__init__.py
index 64c9eac0c4bd31e78ae84e136c80c93579ca13f6..7d8fe86eec28e73d96d2d48bc4e40c149b7684cd 100644
--- a/reviewboard/reviews/ui/__init__.py
+++ b/reviewboard/reviews/ui/__init__.py
@@ -8,10 +8,12 @@ def _register_review_uis(**kwargs):
     from reviewboard.reviews.ui.base import register_ui
     from reviewboard.reviews.ui.image import ImageReviewUI
     from reviewboard.reviews.ui.markdownui import MarkdownReviewUI
+    from reviewboard.reviews.ui.jsonui import JsonReviewUI
     from reviewboard.reviews.ui.text import TextBasedReviewUI
 
     register_ui(ImageReviewUI)
     register_ui(MarkdownReviewUI)
+    register_ui(JsonReviewUI)
     register_ui(TextBasedReviewUI)
 
 
diff --git a/reviewboard/reviews/ui/jsonui.py b/reviewboard/reviews/ui/jsonui.py
new file mode 100644
index 0000000000000000000000000000000000000000..49d22f493f73ed41ca614e23c70aa57388efe87b
--- /dev/null
+++ b/reviewboard/reviews/ui/jsonui.py
@@ -0,0 +1,128 @@
+from __future__ import unicode_literals
+
+import logging
+import json
+from collections import OrderedDict
+
+from pygments import highlight
+from pygments.lexers.data import JsonLexer
+from djblets.cache.backend import cache_memoize
+from reviewboard.diffviewer.chunk_generator import NoWrapperHtmlFormatter
+from reviewboard.reviews.ui.text import TextBasedReviewUI
+from reviewboard.settings import _
+
+
+def render_json(text, sorted_keys):
+    """Renders JSON text to highlighted HTML.
+    """
+    print(text)
+    print(sorted_keys)
+    formatted = json.dumps(text, indent=4, sort_keys=sorted_keys)
+    print(formatted)
+    lines = highlight(formatted, JsonLexer(),
+                      NoWrapperHtmlFormatter()).splitlines()
+    return lines
+
+
+def add_pre(text):
+    """Adds <pre> tags to each line in a list of strings
+    if they do not already exist
+    """
+    if '<pre>' not in text[0]:
+        return [
+            '<pre>%s</pre>' % line
+            for line in text
+        ]
+    else:
+        return text
+
+
+class JsonReviewUI(TextBasedReviewUI):
+    """A Review UI for JSON files. This renders the JSON to HTML.
+    """
+    name = 'json'
+    supported_mimetypes = ['application/json']
+    object_key = 'json'
+    can_render_text = True
+    extra_css_classes = ['json-review-ui']
+
+    js_view_class = 'RB.JsonReviewableView'
+    diff_view = True
+
+    def generate_render(self):
+        """Generates a render of the text.
+
+        Overrides generate_render(self) in text.py
+        """
+        with self.obj.file as f:
+            f.open()
+            text = json.load(f, object_pairs_hook=OrderedDict)
+            print(type(text))
+            rendered = render_json(text, True)
+            if self.diff_view is False and self.diff_against_obj is None:
+                rendered = add_pre(rendered)
+                self.diff_view = True
+
+        try:
+            for line in rendered:
+                yield line
+        except Exception as e:
+            logging.error('Failed to parse resulting JSON HTML for '
+                          'file attachment %d: %s',
+                          self.obj.pk, e,
+                          exc_info=True)
+            yield _('Error while rendering Markdown content: %s') % e
+
+    def generate_highlighted_text(self):
+        """Generates syntax-highlighted text for the file.
+
+        This will render the text file to HTML, applying any syntax
+        highlighting that's appropriate. The contents will be split into
+        reviewable lines and will be cached for future renders. This
+        overrides the generate_highlight_text(self) in text.py
+        """
+        self.diff_view = False
+        data = self.get_text()
+        lines = highlight(data, JsonLexer(), NoWrapperHtmlFormatter())\
+            .splitlines()
+        return lines
+
+    def get_source_lexer(self, filename, data):
+        return JsonLexer()
+
+    def get_text_lines(self):
+        """Return the file contents as syntax-highlighted lines.
+
+        This will fetch the file, render it however appropriate for the review
+        UI, and split it into reviewable lines. It will then cache it for
+        future renders. This overrides get_test_lines(self) in text.py file.
+        """
+        lines = cache_memoize('text-attachment-%d-lines' % self.obj.pk,
+                              lambda: list(self.generate_highlighted_text()))
+
+        if '<pre>' not in lines[0]:
+            lines = add_pre(lines)
+
+        if self.can_render_text:
+            return lines
+        else:
+            return []
+
+    def get_rendered_lines(self):
+        """Returns the file contents as a render, based on the raw text.
+
+        If a subclass sets ``can_render_text = True`` and implements
+        ``generate_render``, then this will render the contents in some
+        specialized form, cache it as a list of lines, and return it.
+        This overrides get_rendered_lines(self) in the text.py file.
+        """
+        lines = cache_memoize('text-attachment-%d-rendered' % self.obj.pk,
+                              lambda: list(self.generate_render()))
+
+        if '<pre>' not in lines[0]:
+            lines = add_pre(lines)
+
+        if self.can_render_text:
+            return lines
+        else:
+            return []
diff --git a/reviewboard/static/rb/js/views/jsonReviewableView.es6.js b/reviewboard/static/rb/js/views/jsonReviewableView.es6.js
new file mode 100644
index 0000000000000000000000000000000000000000..b19016c4b1df3b5aff1fc067226142c07d437e36
--- /dev/null
+++ b/reviewboard/static/rb/js/views/jsonReviewableView.es6.js
@@ -0,0 +1,6 @@
+/**
+ * Displays a review UI for Json files.
+ */
+RB.JsonReviewableView = RB.TextBasedReviewableView.extend({
+    className: 'json-review-ui',
+});
diff --git a/reviewboard/staticbundles.py b/reviewboard/staticbundles.py
index 8b2ea1e3b5a70cf3cede52195ab7e9dd35a33557..77e0b9c1d8f2e6cdc46164c5b1a2eb9000125b69 100644
--- a/reviewboard/staticbundles.py
+++ b/reviewboard/staticbundles.py
@@ -292,6 +292,7 @@ PIPELINE_JAVASCRIPT = dict({
             'rb/js/views/textBasedCommentBlockView.es6.js',
             'rb/js/views/textBasedReviewableView.es6.js',
             'rb/js/views/textCommentRowSelector.es6.js',
+            'rb/js/views/jsonReviewableView.es6.js',
             'rb/js/views/markdownReviewableView.es6.js',
             'rb/js/views/uploadDiffView.es6.js',
             'rb/js/views/updateDiffView.es6.js',
