Repository: nautobot/nautobot-app-golden-config (language: python)

Issue:
Compliance on Unstructured Data
### Environment
* Python version: 3.7
* Nautobot version: 1.0
* nautobot-golden-config version: 0.9

### Proposed Functionality

Currently the golden configuration's primary use case is covering the CLI use case. This does not cover the structured data use case of compliance. 

### Use Case

Leveraging modern API based services, to provide structured data compliance in a similar way to what the cli system currently provides. 

@whitej6 is already doing some work in this area, would like to get his insight/input/help if possible. 


Discussion / hints:

Fix patch:
diff --git a/docs/img/00-navigating-compliance-json.png b/docs/img/00-navigating-compliance-json.png
new file mode 100644
index 00000000..a74ce4fc
Binary files /dev/null and b/docs/img/00-navigating-compliance-json.png differ
diff --git a/docs/img/01-navigating-compliance-json.png b/docs/img/01-navigating-compliance-json.png
new file mode 100644
index 00000000..a2221cad
Binary files /dev/null and b/docs/img/01-navigating-compliance-json.png differ
diff --git a/docs/img/02-navigating-compliance-json.png b/docs/img/02-navigating-compliance-json.png
new file mode 100644
index 00000000..8bb16b1c
Binary files /dev/null and b/docs/img/02-navigating-compliance-json.png differ
diff --git a/docs/img/03-navigating-compliance-json.png b/docs/img/03-navigating-compliance-json.png
new file mode 100644
index 00000000..59098129
Binary files /dev/null and b/docs/img/03-navigating-compliance-json.png differ
diff --git a/docs/img/04-navigating-compliance-json.png b/docs/img/04-navigating-compliance-json.png
new file mode 100644
index 00000000..0ad2ed8c
Binary files /dev/null and b/docs/img/04-navigating-compliance-json.png differ
diff --git a/docs/img/05-navigating-compliance-json.png b/docs/img/05-navigating-compliance-json.png
new file mode 100644
index 00000000..011d3ac7
Binary files /dev/null and b/docs/img/05-navigating-compliance-json.png differ
diff --git a/docs/img/06-navigating-compliance-json.png b/docs/img/06-navigating-compliance-json.png
new file mode 100644
index 00000000..b208e200
Binary files /dev/null and b/docs/img/06-navigating-compliance-json.png differ
diff --git a/docs/img/07-navigating-compliance-json.png b/docs/img/07-navigating-compliance-json.png
new file mode 100644
index 00000000..dc897990
Binary files /dev/null and b/docs/img/07-navigating-compliance-json.png differ
diff --git a/docs/img/08-navigating-compliance-json.png b/docs/img/08-navigating-compliance-json.png
new file mode 100644
index 00000000..ce21bddb
Binary files /dev/null and b/docs/img/08-navigating-compliance-json.png differ
diff --git a/docs/navigating-compliance-json.md b/docs/navigating-compliance-json.md
new file mode 100644
index 00000000..6ddf7b8a
--- /dev/null
+++ b/docs/navigating-compliance-json.md
@@ -0,0 +1,81 @@
+# Navigating Compliance Using Structured Data
+
+> Warning:
+This feature is in early development and is only implemented within the API.  There are multiple caveats that need to be considered and they are listed below.
+
+## Caveats
+- Must have a `Platform` created specifically for the feature.
+- The compliance `rule` need to be unique for the JSON `config-type`.
+- When creating `Compliance Rules` with the config type of JSON, the `config to match` field is not used it should be left blank.
+- Does **NOT** use Git repositories for backup and intended configurations.
+- Mixing/Matching CLI and JSON compliance rules is **NOT** supported.  A device should only have compliance rules from one or the other, and it is up to the operator to enforce.
+
+## Quick Start Guide
+
+1. To get started with the structured data(JSON) compliance check the first step is to create a platform.
+
+![Example Platform Creation](./img/00-navigating-compliance-json.png)
+
+2. Next, the compliance feature needs to be created, the feature can be shared between a CLI rule and a JSON rule.
+
+![Example Feature Creation](./img/01-navigating-compliance-json.png)
+
+3. Link the feature that was just created to a rule definition.
+![Example Rule Creation](./img/02-navigating-compliance-json.png)
+
+4. Now that the definitions are created and the rule is mapped to the platform we created the API execution can be performed.
+
+Here is an example of a `POST` to the `/plugins​/golden-config​/config-compliance​/` endpoint.
+
+```json
+{
+  "actual": {"aaa": {"servers": ["1.1.1.1"]}},
+  "intended": {"aaa": {"servers": ["1.1.1.1"]}},
+  "device": "472c3786-4499-4e34-8d8e-291c4c7f32a9",
+  "rule": "87cc003b-9025-472d-bf3c-e5b62bf91987"
+}
+```
+
+> Notice: The UUID is needed for the device and rule to execute against.  This data can be found in the UI or by querying the proper endpoints, `/plugins/golden-config/compliance-rule/` and `/dcim​/devices​/`.
+
+Once the API call is made the response data provides a quick snapshot.
+```json
+{
+  "id": "be35a701-0cb0-418a-98e7-a82d99db857e",
+  "device": "472c3786-4499-4e34-8d8e-291c4c7f32a9",
+  "rule": "87cc003b-9025-472d-bf3c-e5b62bf91987",
+  "actual": {"aaa": {"servers": ["1.1.1.1"]}},
+  "intended": {"aaa": {"servers": ["1.1.1.1"]}},
+  "ordered": true,
+  "compliance": true,
+  "display": "nautobot-iosv-1 -> iosv-json - aaa-json -> True"
+}
+```
+
+You can also see the compliance data in the UI once it is created via API.
+
+In the navigation menu: `Plugins -> Configuration Compliance`.
+
+![Example Compliance Run in UI](./img/03-navigating-compliance-json.png)
+
+With the detailed view:
+
+![Example Compliance Run in UI Detail](./img/04-navigating-compliance-json.png)
+
+Example of a Non-Compliant rule:
+
+![Example Non-Compliant Run in UI Detail](./img/05-navigating-compliance-json.png)
+
+> Note:
+All subsequent updates to the compliance rule need to use the `PUT` or `PATCH` API methods and they need to provide the UUID that was created for the specific compliance execution. This UUID is generated by (unique_together = ("device", "rule"))
+
+![Example of Diff Icon](./img/06-navigating-compliance-json.png)
+
+Seeing the diff button alone will **only** show up for devices using JSON compliance rules.
+
+
+![Show Updated Overview](./img/07-navigating-compliance-json.png)
+
+The detailed diff view will show a side by side diff, this looks the same as the CLI view.
+
+![Detail Diff View](./img/08-navigating-compliance-json.png)
diff --git a/docs/navigating-compliance.md b/docs/navigating-compliance.md
index 2d2a749b..54ffbff1 100644
--- a/docs/navigating-compliance.md
+++ b/docs/navigating-compliance.md
@@ -1,5 +1,8 @@
 # Configuration Compliance 
 
+> Note:
+This document provides instructions based on `CLI` based compliance.  The other option is `JSON` [structured data compliance](./navigating-compliance-json.md).
+
 The following should be noted by what is meant by configuration compliance. Configurations are considered to be compliant if the generated configuration 
 (generally by merging data and Jinja2, will be referred to as the intended configuration from hence forth) matches "exactly" as the actual configuration is 
 on the backup. This may confusing to some, as for example to the average network engineer, there is no difference between `int g0/0` and
diff --git a/nautobot_golden_config/api/serializers.py b/nautobot_golden_config/api/serializers.py
index 5f3b4baa..0546fc67 100644
--- a/nautobot_golden_config/api/serializers.py
+++ b/nautobot_golden_config/api/serializers.py
@@ -1,6 +1,7 @@
 """REST API serializer capabilities for graphql plugin."""
 # pylint: disable=too-many-ancestors
 from rest_framework import serializers
+
 from nautobot.extras.api.customfields import CustomFieldModelSerializer
 from nautobot.extras.api.serializers import TaggedObjectSerializer
 
@@ -42,12 +43,8 @@ class Meta:
 class ConfigComplianceSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
     """Serializer for ConfigCompliance object."""
 
-    url = serializers.HyperlinkedIdentityField(
-        view_name="plugins-api:nautobot_golden_config-api:configcompliance-detail"
-    )
-
     class Meta:
-        """Set Meta Data for ConfigCompliance, will serialize all fields."""
+        """Set Meta Data for ConfigCompliance, will serialize fields."""
 
         model = models.ConfigCompliance
         fields = "__all__"
diff --git a/nautobot_golden_config/filters.py b/nautobot_golden_config/filters.py
index 722d4280..1bbd77bb 100644
--- a/nautobot_golden_config/filters.py
+++ b/nautobot_golden_config/filters.py
@@ -2,7 +2,7 @@
 
 import django_filters
 
-from django.db.models import Q, Subquery
+from django.db.models import Q
 
 from nautobot.dcim.models import Device, Platform, Region, Site, DeviceRole, DeviceType, Manufacturer, RackGroup, Rack
 from nautobot.extras.models import Status
@@ -22,37 +22,37 @@ class GoldenConfigFilter(CreatedUpdatedFilterSet):
     )
     tenant_group_id = TreeNodeMultipleChoiceFilter(
         queryset=TenantGroup.objects.all(),
-        field_name="device__tenant__group",
+        field_name="tenant__group",
         lookup_expr="in",
         label="Tenant Group (ID)",
     )
     tenant_group = TreeNodeMultipleChoiceFilter(
         queryset=TenantGroup.objects.all(),
-        field_name="device__tenant__group",
+        field_name="tenant__group",
         to_field_name="slug",
         lookup_expr="in",
         label="Tenant Group (slug)",
     )
     tenant_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Tenant.objects.all(),
-        field_name="device__tenant_id",
+        field_name="tenant_id",
         label="Tenant (ID)",
     )
     tenant = django_filters.ModelMultipleChoiceFilter(
         queryset=Tenant.objects.all(),
-        field_name="device__tenant__slug",
+        field_name="tenant__slug",
         to_field_name="slug",
         label="Tenant (slug)",
     )
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
-        field_name="device__site__region",
+        field_name="site__region",
         lookup_expr="in",
         label="Region (ID)",
     )
     region = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
-        field_name="device__site__region",
+        field_name="site__region",
         lookup_expr="in",
         to_field_name="slug",
         label="Region (slug)",
@@ -62,40 +62,40 @@ class GoldenConfigFilter(CreatedUpdatedFilterSet):
         label="Site (ID)",
     )
     site = django_filters.ModelMultipleChoiceFilter(
-        field_name="device__site__slug",
+        field_name="site__slug",
         queryset=Site.objects.all(),
         to_field_name="slug",
         label="Site name (slug)",
     )
     rack_group_id = TreeNodeMultipleChoiceFilter(
         queryset=RackGroup.objects.all(),
-        field_name="device__rack__group",
+        field_name="rack__group",
         lookup_expr="in",
         label="Rack group (ID)",
     )
     rack_id = django_filters.ModelMultipleChoiceFilter(
-        field_name="device__rack",
+        field_name="rack",
         queryset=Rack.objects.all(),
         label="Rack (ID)",
     )
     role_id = django_filters.ModelMultipleChoiceFilter(
-        field_name="device__device_role_id",
+        field_name="device_role_id",
         queryset=DeviceRole.objects.all(),
         label="Role (ID)",
     )
     role = django_filters.ModelMultipleChoiceFilter(
-        field_name="device__device_role__slug",
+        field_name="device_role__slug",
         queryset=DeviceRole.objects.all(),
         to_field_name="slug",
         label="Role (slug)",
     )
     manufacturer_id = django_filters.ModelMultipleChoiceFilter(
-        field_name="device__device_type__manufacturer",
+        field_name="device_type__manufacturer",
         queryset=Manufacturer.objects.all(),
         label="Manufacturer (ID)",
     )
     manufacturer = django_filters.ModelMultipleChoiceFilter(
-        field_name="device__device_type__manufacturer__slug",
+        field_name="device_type__manufacturer__slug",
         queryset=Manufacturer.objects.all(),
         to_field_name="slug",
         label="Manufacturer (slug)",
@@ -105,28 +105,28 @@ class GoldenConfigFilter(CreatedUpdatedFilterSet):
         label="Platform (ID)",
     )
     platform = django_filters.ModelMultipleChoiceFilter(
-        field_name="device__platform__slug",
+        field_name="platform__slug",
         queryset=Platform.objects.all(),
         to_field_name="slug",
         label="Platform (slug)",
     )
     device_status_id = StatusFilter(
-        field_name="device__status",
+        field_name="status",
         queryset=Status.objects.all(),
         label="Device Status",
     )
     device_type_id = django_filters.ModelMultipleChoiceFilter(
-        field_name="device__device_type_id",
+        field_name="device_type_id",
         queryset=DeviceType.objects.all(),
         label="Device type (ID)",
     )
     device_id = django_filters.ModelMultipleChoiceFilter(
         field_name="device",
         queryset=Device.objects.all(),
-        label="Device Name",
+        label="Device (ID)",
     )
     device = django_filters.ModelMultipleChoiceFilter(
-        field_name="device__name",
+        field_name="name",
         queryset=Device.objects.all(),
         label="Device Name",
     )
@@ -136,13 +136,13 @@ def search(self, queryset, name, value):  # pylint: disable=unused-argument,no-s
         if not value.strip():
             return queryset
         # Chose only device, can be convinced more should be included
-        qs_filter = Q(device__name__icontains=value)
+        qs_filter = Q(name__icontains=value)
         return queryset.filter(qs_filter)
 
     class Meta:
         """Meta class attributes for GoldenConfig."""
 
-        model = models.GoldenConfig
+        model = Device
         distinct = True
         fields = [
             "q",
@@ -169,18 +169,131 @@ class Meta:
         ]
 
 
-class ConfigComplianceFilter(GoldenConfigFilter):
+class ConfigComplianceFilter(CreatedUpdatedFilterSet):
     """Filter capabilities for ConfigCompliance instances."""
 
+    q = django_filters.CharFilter(
+        method="search",
+        label="Search",
+    )
+    tenant_group_id = TreeNodeMultipleChoiceFilter(
+        queryset=TenantGroup.objects.all(),
+        field_name="device__tenant__group",
+        lookup_expr="in",
+        label="Tenant Group (ID)",
+    )
+    tenant_group = TreeNodeMultipleChoiceFilter(
+        queryset=TenantGroup.objects.all(),
+        field_name="device__tenant__group",
+        to_field_name="slug",
+        lookup_expr="in",
+        label="Tenant Group (slug)",
+    )
+    tenant_id = django_filters.ModelMultipleChoiceFilter(
+        queryset=Tenant.objects.all(),
+        field_name="device__tenant_id",
+        label="Tenant (ID)",
+    )
+    tenant = django_filters.ModelMultipleChoiceFilter(
+        queryset=Tenant.objects.all(),
+        field_name="device__tenant__slug",
+        to_field_name="slug",
+        label="Tenant (slug)",
+    )
+    region_id = TreeNodeMultipleChoiceFilter(
+        queryset=Region.objects.all(),
+        field_name="device__site__region",
+        lookup_expr="in",
+        label="Region (ID)",
+    )
+    region = TreeNodeMultipleChoiceFilter(
+        queryset=Region.objects.all(),
+        field_name="device__site__region",
+        lookup_expr="in",
+        to_field_name="slug",
+        label="Region (slug)",
+    )
+    site_id = django_filters.ModelMultipleChoiceFilter(
+        queryset=Site.objects.all(),
+        label="Site (ID)",
+    )
+    site = django_filters.ModelMultipleChoiceFilter(
+        field_name="device__site__slug",
+        queryset=Site.objects.all(),
+        to_field_name="slug",
+        label="Site name (slug)",
+    )
+    rack_group_id = TreeNodeMultipleChoiceFilter(
+        queryset=RackGroup.objects.all(),
+        field_name="device__rack__group",
+        lookup_expr="in",
+        label="Rack group (ID)",
+    )
+    rack_id = django_filters.ModelMultipleChoiceFilter(
+        field_name="device__rack",
+        queryset=Rack.objects.all(),
+        label="Rack (ID)",
+    )
+    role_id = django_filters.ModelMultipleChoiceFilter(
+        field_name="device__device_role_id",
+        queryset=DeviceRole.objects.all(),
+        label="Role (ID)",
+    )
+    role = django_filters.ModelMultipleChoiceFilter(
+        field_name="device__device_role__slug",
+        queryset=DeviceRole.objects.all(),
+        to_field_name="slug",
+        label="Role (slug)",
+    )
+    manufacturer_id = django_filters.ModelMultipleChoiceFilter(
+        field_name="device__device_type__manufacturer",
+        queryset=Manufacturer.objects.all(),
+        label="Manufacturer (ID)",
+    )
+    manufacturer = django_filters.ModelMultipleChoiceFilter(
+        field_name="device__device_type__manufacturer__slug",
+        queryset=Manufacturer.objects.all(),
+        to_field_name="slug",
+        label="Manufacturer (slug)",
+    )
+    platform_id = django_filters.ModelMultipleChoiceFilter(
+        queryset=Platform.objects.all(),
+        label="Platform (ID)",
+    )
+    platform = django_filters.ModelMultipleChoiceFilter(
+        field_name="device__platform__slug",
+        queryset=Platform.objects.all(),
+        to_field_name="slug",
+        label="Platform (slug)",
+    )
+    device_status_id = StatusFilter(
+        field_name="device__status",
+        queryset=Status.objects.all(),
+        label="Device Status",
+    )
+    device_type_id = django_filters.ModelMultipleChoiceFilter(
+        field_name="device__device_type_id",
+        queryset=DeviceType.objects.all(),
+        label="Device type (ID)",
+    )
+    device_id = django_filters.ModelMultipleChoiceFilter(
+        queryset=Device.objects.all(),
+        label="Device Name",
+    )
     device = django_filters.ModelMultipleChoiceFilter(
         field_name="device__name",
-        queryset=Device.objects.filter(
-            id__in=Subquery(models.ConfigCompliance.objects.distinct("device").values("device"))
-        ),
-        to_field_name="name",
+        queryset=Device.objects.all(),
         label="Device Name",
     )
 
+    def search(self, queryset, name, value):  # pylint: disable=unused-argument,no-self-use
+        """Perform the filtered search."""
+        if not value.strip():
+            return queryset
+        # Chose only device, can be convinced more should be included
+        qs_filter = Q(device__name__icontains=value)
+        return queryset.filter(qs_filter)
+
     class Meta:
         """Meta class attributes for ConfigComplianceFilter."""
 
diff --git a/nautobot_golden_config/forms.py b/nautobot_golden_config/forms.py
index c0eacac2..2fa46376 100644
--- a/nautobot_golden_config/forms.py
+++ b/nautobot_golden_config/forms.py
@@ -1,7 +1,6 @@
 """Forms for Device Configuration Backup."""
 
 from django import forms
-from django.db.models import Subquery
 
 import nautobot.extras.forms as extras_forms
 import nautobot.utilities.forms as utilities_forms
@@ -15,7 +14,7 @@
 class GoldenConfigFilterForm(utilities_forms.BootstrapMixin, extras_forms.CustomFieldFilterForm):
     """Filter Form for GoldenConfig instances."""
 
-    model = models.GoldenConfig
+    model = Device
 
     class Meta:
         """Meta definitions of searchable fields."""
@@ -106,14 +105,6 @@ class ConfigComplianceFilterForm(GoldenConfigFilterForm):
     """Filter Form for ConfigCompliance instances."""
 
     model = models.ConfigCompliance
-    device = utilities_forms.DynamicModelMultipleChoiceField(
-        queryset=Device.objects.filter(
-            id__in=Subquery(models.ConfigCompliance.objects.distinct("device").values("device"))
-        ),
-        to_field_name="name",
-        required=False,
-        null_option="None",
-    )
 
 
 # ComplianceRule
diff --git a/nautobot_golden_config/migrations/0005_json_compliance_rule.py b/nautobot_golden_config/migrations/0005_json_compliance_rule.py
new file mode 100644
index 00000000..3bda4f90
--- /dev/null
+++ b/nautobot_golden_config/migrations/0005_json_compliance_rule.py
@@ -0,0 +1,52 @@
+from django.db import migrations, models
+import json
+
+from nautobot_golden_config.models import ConfigCompliance
+
+
+def jsonify(apps, schedma_editor):
+    """Converts textfield to json in preparation for migration."""
+    queryset = ConfigCompliance.objects.all()
+    attrs = ["actual", "extra", "intended", "missing"]
+    for i in queryset:
+        for attr in attrs:
+            value = getattr(i, attr)
+            if value:
+                setattr(i, attr, json.dumps(value))
+        i.save()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("nautobot_golden_config", "0004_auto_20210616_2234"),
+    ]
+
+    operations = [
+        migrations.RunPython(code=jsonify),
+        migrations.AlterField(
+            model_name="compliancerule",
+            name="match_config",
+            field=models.TextField(blank=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name="configcompliance",
+            name="actual",
+            field=models.JSONField(blank=True),
+        ),
+        migrations.AlterField(
+            model_name="configcompliance",
+            name="extra",
+            field=models.JSONField(blank=True),
+        ),
+        migrations.AlterField(
+            model_name="configcompliance",
+            name="intended",
+            field=models.JSONField(blank=True),
+        ),
+        migrations.AlterField(
+            model_name="configcompliance",
+            name="missing",
+            field=models.JSONField(blank=True),
+        ),
+    ]
diff --git a/nautobot_golden_config/models.py b/nautobot_golden_config/models.py
index d7d79a4e..3e77c9de 100644
--- a/nautobot_golden_config/models.py
+++ b/nautobot_golden_config/models.py
@@ -1,6 +1,7 @@
 """Django Models for tracking the configuration compliance per feature and device."""
 
 import logging
+from deepdiff import DeepDiff
 
 from django.db import models
 from django.core.exceptions import ValidationError
@@ -98,7 +99,7 @@ class ComplianceRule(PrimaryModel):
     )
     match_config = models.TextField(
         null=True,
-        blank=False,
+        blank=True,
         verbose_name="Config to Match",
         help_text="The config to match that is matched based on the parent most configuration. e.g. `router bgp` or `ntp`.",
     )
@@ -143,8 +144,6 @@ def clean(self):
         """Verify that if cli, then match_config is set."""
         if self.config_type == ComplianceRuleTypeChoice.TYPE_CLI and not self.match_config:
             raise ValidationError("CLI configuration set, but no configuration set to match.")
-        if self.config_type == ComplianceRuleTypeChoice.TYPE_JSON:
-            raise ValidationError("JSON currently not supported.")
 
 
 @extras_features(
@@ -162,11 +161,12 @@ class ConfigCompliance(PrimaryModel):
     device = models.ForeignKey(to="dcim.Device", on_delete=models.CASCADE, help_text="The device", blank=False)
     rule = models.ForeignKey(to="ComplianceRule", on_delete=models.CASCADE, blank=False, related_name="rule")
     compliance = models.BooleanField(null=True, blank=True)
-    actual = models.TextField(blank=True, help_text="Actual Configuration for feature")
-    intended = models.TextField(blank=True, help_text="Intended Configuration for feature")
-    missing = models.TextField(blank=True, help_text="Configuration that should be on the device.")
-    extra = models.TextField(blank=True, help_text="Configuration that should not be on the device.")
+    actual = models.JSONField(blank=True, help_text="Actual Configuration for feature")
+    intended = models.JSONField(blank=True, help_text="Intended Configuration for feature")
+    missing = models.JSONField(blank=True, help_text="Configuration that should be on the device.")
+    extra = models.JSONField(blank=True, help_text="Configuration that should not be on the device.")
     ordered = models.BooleanField(default=True)
+    # Used for django-pivot, both compliance and compliance_int should be set.
     compliance_int = models.IntegerField(null=True, blank=True)
 
     csv_headers = ["Device Name", "Feature", "Compliance"]
@@ -199,23 +199,47 @@ def __str__(self):
         return f"{self.device} -> {self.rule} -> {self.compliance}"
 
     def save(self, *args, **kwargs):
-        """Perform that actual compliance check."""
+        """Performs the actual compliance check."""
         feature = {
             "ordered": self.rule.config_ordered,
-            "name": self.rule.feature.name,
-            "section": self.rule.match_config.splitlines(),
+            "name": self.rule,
         }
-        value = feature_compliance(feature, self.actual, self.intended, get_platform(self.device.platform.slug))
-        self.compliance = value["compliant"]
-        if self.compliance:
-            self.compliance_int = 1
+        if self.rule.config_type == ComplianceRuleTypeChoice.TYPE_JSON:
+            feature.update({"section": self.rule.match_config})
+
+            diff = DeepDiff(self.actual, self.intended, ignore_order=self.ordered, report_repetition=True)
+            if not diff:
+                self.compliance_int = 1
+                self.compliance = True
+                self.missing = ""
+                self.extra = ""
+            else:
+                self.compliance_int = 0
+                self.compliance = False
+                self.missing = null_to_empty(self._normalize_diff(diff, "added"))
+                self.extra = null_to_empty(self._normalize_diff(diff, "removed"))
         else:
-            self.compliance_int = 0
-        self.ordered: value["ordered_compliant"]
-        self.missing = null_to_empty(value["missing"])
-        self.extra = null_to_empty(value["extra"])
+            feature.update({"section": self.rule.match_config.splitlines()})
+            value = feature_compliance(feature, self.actual, self.intended, get_platform(self.device.platform.slug))
+            self.compliance = value["compliant"]
+            if self.compliance:
+                self.compliance_int = 1
+            else:
+                self.compliance_int = 0
+                self.ordered = value["ordered_compliant"]
+                self.missing = null_to_empty(value["missing"])
+                self.extra = null_to_empty(value["extra"])
         super().save(*args, **kwargs)
 
+    @staticmethod
+    def _normalize_diff(diff, path_to_diff):
+        """Normalizes the diff to a list of keys and list indexes that have changed."""
+        dictionary_items = list(diff.get(f"dictionary_item_{path_to_diff}", []))
+        list_items = list(diff.get(f"iterable_item_{path_to_diff}", {}).keys())
+        values_changed = list(diff.get("values_changed", {}).keys())
+        type_changes = list(diff.get("type_changes", {}).keys())
+        return dictionary_items + list_items + values_changed + type_changes
+
 
 @extras_features(
     "custom_fields",
diff --git a/nautobot_golden_config/nornir_plays/config_compliance.py b/nautobot_golden_config/nornir_plays/config_compliance.py
index 18dfea6a..1e7845ba 100644
--- a/nautobot_golden_config/nornir_plays/config_compliance.py
+++ b/nautobot_golden_config/nornir_plays/config_compliance.py
@@ -35,7 +35,7 @@ def get_features():
     """A serializer of sorts to return feature mappings as a dictionary."""
     # TODO: Review if creating a proper serializer is the way to go.
     features = {}
-    for obj in ComplianceRule.objects.all():
+    for obj in ComplianceRule.objects.filter(config_type="cli"):
         platform = str(obj.platform.slug)
         if not features.get(platform):
             features[platform] = []
@@ -112,6 +112,8 @@ def run_compliance(  # pylint: disable=too-many-arguments,too-many-locals
             defaults={
                 "actual": section_config(feature, backup_cfg, get_platform(platform)),
                 "intended": section_config(feature, intended_cfg, get_platform(platform)),
+                "missing": "",
+                "extra": "",
             },
         )
 
diff --git a/nautobot_golden_config/tables.py b/nautobot_golden_config/tables.py
index cbbf46c9..abb1834d 100644
--- a/nautobot_golden_config/tables.py
+++ b/nautobot_golden_config/tables.py
@@ -5,6 +5,7 @@
 from django_tables2 import Column, LinkColumn, TemplateColumn
 from django_tables2.utils import A
 
+from nautobot.dcim.models import Device
 from nautobot.utilities.tables import (
     BaseTable,
     ToggleColumn,
@@ -12,89 +13,62 @@
 from nautobot_golden_config import models
 from nautobot_golden_config.utilities.constant import ENABLE_BACKUP, ENABLE_COMPLIANCE, ENABLE_INTENDED, CONFIG_FEATURES
 
-BACKUP_SUCCESS = """
-{% if record.backup_last_success_date and record.backup_last_attempt_date == record.backup_last_success_date %}
-    <span class="text-success" id="actions">
-{% else %}
-    <span class="text-danger" id="actions">
-{% endif %}
-{% if record.backup_last_success_date %}
-        {{ record.backup_last_success_date|date:"SHORT_DATETIME_FORMAT" }}
-{% else %}
-    --
-{% endif %}
-        <span id=actiontext>{{ record.backup_last_attempt_date|date:"SHORT_DATETIME_FORMAT" }}</span>
-    </span>
-"""
-
-INTENDED_SUCCESS = """
-{% if record.intended_last_success_date and record.intended_last_attempt_date == record.intended_last_success_date %}
-    <span class="text-success" id="actions">
-{% else %}
-    <span class="text-danger" id="actions">
-{% endif %}
-{% if record.intended_last_success_date %}
-        {{ record.intended_last_success_date|date:"SHORT_DATETIME_FORMAT" }}
-{% else %}
-    --
-{% endif %}
-        <span id=actiontext>{{ record.intended_last_attempt_date|date:"SHORT_DATETIME_FORMAT" }}</span>
-    </span>
-"""
-
-
-COMPLIANCE_SUCCESS = """
-{% if record.compliance_last_success_date and record.compliance_last_attempt_date == record.compliance_last_success_date %}
-    <span class="text-success" id="actions">
-{% else %}
-    <span class="text-danger" id="actions">
-{% endif %}
-{% if record.compliance_last_success_date %}
-        {{ record.compliance_last_success_date|date:"SHORT_DATETIME_FORMAT" }}
-{% else %}
-    --
-{% endif %}
-        <span id=actiontext>{{ record.compliance_last_attempt_date|date:"SHORT_DATETIME_FORMAT" }}</span>
-    </span>
-"""
 
 ALL_ACTIONS = """
 {% if backup == True %}
-    {% if record.backup_config %}
-        <a value="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.device.pk config_type='backup' %}" class="openBtn" data-href="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.device.pk config_type='backup' %}?modal=true"> 
-            <i class="mdi mdi-file-document-outline" title="Backup Configuration"></i>
-        </a>
-    {% else %}
+    {% if record.configcompliance_set.first.rule.config_type == 'json' %}
         <i class="mdi mdi-circle-small"></i>
+    {% else %}
+        {% if record.goldenconfig_set.first.backup_config %}
+            <a value="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.pk config_type='backup' %}" class="openBtn" data-href="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.pk config_type='backup' %}?modal=true">
+                <i class="mdi mdi-file-document-outline" title="Backup Configuration"></i>
+            </a>
+        {% else %}
+            <i class="mdi mdi-circle-small"></i>
+        {% endif %}
     {% endif %}
 {% endif %}
 {% if intended == True %}
-    {% if record.intended_config %}
-        <a value="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.device.pk config_type='intended' %}" class="openBtn" data-href="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.device.pk config_type='intended' %}?modal=true"> 
-            <i class="mdi mdi-text-box-check-outline" title="Intended Configuration"></i>
-        </a>
-    {% else %}
+    {% if record.configcompliance_set.first.rule.config_type == 'json' %}
         <i class="mdi mdi-circle-small"></i>
+    {% else %}
+        {% if record.goldenconfig_set.first.intended_config %}
+            <a value="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.pk config_type='intended' %}" class="openBtn" data-href="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.pk config_type='intended' %}?modal=true">
+                <i class="mdi mdi-text-box-check-outline" title="Intended Configuration"></i>
+            </a>
+        {% else %}
+            <i class="mdi mdi-circle-small"></i>
+        {% endif %}
     {% endif %}
 {% endif %}
 {% if compliance == True %}
-    {% if record.compliance_config %}
-        <a value="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.device.pk config_type='compliance' %}" class="openBtn" data-href="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.device.pk config_type='compliance' %}?modal=true"> 
-            <i class="mdi mdi-file-compare" title="Compliance Details"></i>
-        </a>
+    {% if record.configcompliance_set.first.rule.config_type == 'json' %}
+            <a value="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.pk config_type='json_compliance' %}" class="openBtn" data-href="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.pk config_type='json_compliance' %}?modal=true">
+                <i class="mdi mdi-file-compare" title="Compliance Details JSON"></i>
+            </a>
     {% else %}
-        <i class="mdi mdi-circle-small"></i>
+        {% if record.goldenconfig_set.first.compliance_config %}
+            <a value="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.pk config_type='compliance' %}" class="openBtn" data-href="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.pk config_type='compliance' %}?modal=true">
+                <i class="mdi mdi-file-compare" title="Compliance Details"></i>
+            </a>
+        {% else %}
+            <i class="mdi mdi-circle-small"></i>
+        {% endif %}
     {% endif %}
 {% endif %}
 {% if sotagg == True %}
-    <a value="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.device.pk config_type='sotagg' %}" class="openBtn" data-href="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.device.pk config_type='sotagg' %}?modal=true"> 
+    <a value="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.pk config_type='sotagg' %}" class="openBtn" data-href="{% url 'plugins:nautobot_golden_config:configcompliance_details' pk=record.pk config_type='sotagg' %}?modal=true">
         <i class="mdi mdi-code-json" title="SOT Aggregate Data"></i>
     </a>
-    <a href="{% url 'extras:job' class_path='plugins/nautobot_golden_config.jobs/AllGoldenConfig' %}?device={{ record.device.pk }}"
-        <span class="text-primary">
-            <i class="mdi mdi-play-circle" title="Execute All Golden Config Jobs"></i>
-        </span>
-    </a>
+    {% if record.configcompliance_set.first.rule.config_type == 'json' %}
+        <i class="mdi mdi-circle-small"></i>
+    {% else %}
+        <a href="{% url 'extras:job' class_path='plugins/nautobot_golden_config.jobs/AllGoldenConfig' %}?device={{ record.pk }}"
+            <span class="text-primary">
+                <i class="mdi mdi-play-circle" title="Execute All Golden Config Jobs"></i>
+            </span>
+        </a>
+    {% endif %}
 {% endif %}
 """
 
@@ -103,7 +77,7 @@
 
 def actual_fields():
     """Convienance function to conditionally toggle columns."""
-    active_fields = ["pk", "device__name"]
+    active_fields = ["pk", "name"]
     if ENABLE_BACKUP:
         active_fields.append("backup_last_success_date")
     if ENABLE_INTENDED:
@@ -227,27 +201,55 @@ class GoldenConfigTable(BaseTable):
     """Table to display Config Management Status."""
 
     pk = ToggleColumn()
-    device__name = TemplateColumn(
-        template_code="""<a href="{% url 'dcim:device' pk=record.device.pk %}">{{ record.device }}</a>""",
+    name = TemplateColumn(
+        template_code="""<a href="{% url 'dcim:device' pk=record.pk %}">{{ record.name }}</a>""",
         verbose_name="Device",
     )
+
     if ENABLE_BACKUP:
-        backup_last_success_date = TemplateColumn(verbose_name="Backup Status", template_code=BACKUP_SUCCESS)
+        backup_last_success_date = Column(verbose_name="Backup Status", empty_values=())
     if ENABLE_INTENDED:
-        intended_last_success_date = TemplateColumn(verbose_name="Intended Status", template_code=INTENDED_SUCCESS)
+        intended_last_success_date = Column(verbose_name="Intended Status", empty_values=())
     if ENABLE_COMPLIANCE:
-        compliance_last_success_date = TemplateColumn(
-            verbose_name="Compliance Status", template_code=COMPLIANCE_SUCCESS
-        )
+        compliance_last_success_date = Column(verbose_name="Compliance Status", empty_values=())
 
     actions = TemplateColumn(
         template_code=ALL_ACTIONS, verbose_name="Actions", extra_context=CONFIG_FEATURES, orderable=False
     )
 
+    def _render_last_success_date(self, record, column, value):  # pylint: disable=no-self-use
+        """Abstract method to get last success per row record."""
+        entry = record.goldenconfig_set.first()
+        last_success_date = getattr(entry, f"{value}_last_success_date", None)
+        last_attempt_date = getattr(entry, f"{value}_last_attempt_date", None)
+        if not last_success_date or not last_attempt_date:
+            column.attrs = {"td": {"style": "color:black"}}
+            return "--"
+        if not last_success_date and not last_attempt_date:
+            column.attrs = {"td": {"style": "color:black"}}
+            return "--"
+        if last_success_date and last_attempt_date == last_success_date:
+            column.attrs = {"td": {"style": "color:green"}}
+            return last_success_date
+        column.attrs = {"td": {"style": "color:red"}}
+        return last_success_date
+
+    def render_backup_last_success_date(self, record, column):  # pylint: disable=no-self-use
+        """Pull back backup last success per row record."""
+        return self._render_last_success_date(record, column, "backup")
+
+    def render_intended_last_success_date(self, record, column):  # pylint: disable=no-self-use
+        """Pull back intended last success per row record."""
+        return self._render_last_success_date(record, column, "intended")
+
+    def render_compliance_last_success_date(self, record, column):  # pylint: disable=no-self-use
+        """Pull back compliance last success per row record."""
+        return self._render_last_success_date(record, column, "compliance")
+
     class Meta(BaseTable.Meta):
-        """Meta for class CircuitMaintenanceTable."""
+        """Meta for class GoldenConfigTable."""
 
-        model = models.GoldenConfig
+        model = Device
         fields = actual_fields()
 
 
diff --git a/nautobot_golden_config/templates/nautobot_golden_config/configcompliancedetails.html b/nautobot_golden_config/templates/nautobot_golden_config/configcompliancedetails.html
index bc75b270..8337652e 100644
--- a/nautobot_golden_config/templates/nautobot_golden_config/configcompliancedetails.html
+++ b/nautobot_golden_config/templates/nautobot_golden_config/configcompliancedetails.html
@@ -14,7 +14,7 @@
 </style>
 {% if config_type == "sotagg" %}
     <h1>Aggregate Data - {{ device_name }}</h1>
-{% elif config_type == "compliance" %}
+{% elif config_type in "compliance,json_compliance" %}
     <h1>Compliance Details - {{ device_name }}</h1>
 {% elif config_type == "backup" %}
     <h1>Backup Configuration Details - {{ device_name }}</h1>
@@ -33,7 +33,7 @@ <h1>Intended Configuration Details - {{ device_name }}</h1>
                     {% include 'extras/inc/configcontext_format.html' %}
                 </div>
                 <pre>{{ output }}</pre>
-            {% elif  config_type == "compliance" %}
+            {% elif config_type in "compliance,json_compliance" %}
                 <div id="output", value="{{ output }}"></div>
                 <div id="diff"></div>
             {% else %}
diff --git a/nautobot_golden_config/templates/nautobot_golden_config/configcompliancedetails_modal.html b/nautobot_golden_config/templates/nautobot_golden_config/configcompliancedetails_modal.html
index e3ef9c77..928a0145 100644
--- a/nautobot_golden_config/templates/nautobot_golden_config/configcompliancedetails_modal.html
+++ b/nautobot_golden_config/templates/nautobot_golden_config/configcompliancedetails_modal.html
@@ -10,7 +10,7 @@
 </style>
 {% if config_type == "sotagg"%}
     <h1>Aggregate Data - {{ device_name }}</h1>
-{% elif config_type == "compliance"%}
+{% elif "compliance" in config_type %}
     <h1>Compliance Details - {{ device_name }}</h1>
 {% elif config_type == "backup"%}
     <h1>Backup Configuration Details - {{ device_name }}</h1>
@@ -18,7 +18,7 @@ <h1>Backup Configuration Details - {{ device_name }}</h1>
     <h1>Intended Configuration Details - {{ device_name }}</h1>
 {% endif %}
     <div>
-        {% if  config_type == "compliance" %}
+        {% if  "compliance" in config_type %}
             <div id="output", value="{{ output }}"></div>
             <div id="diff"></div>
         {% else %}
diff --git a/nautobot_golden_config/views.py b/nautobot_golden_config/views.py
index 9ee2daa6..9016a974 100644
--- a/nautobot_golden_config/views.py
+++ b/nautobot_golden_config/views.py
@@ -1,11 +1,12 @@
 """Django views for Nautobot Golden Configuration."""
-from datetime import datetime
+from datetime import datetime, timezone
 
 import base64
 import io
 import json
 import logging
 import urllib
+import difflib
 import yaml
 
 import matplotlib.pyplot as plt
@@ -19,6 +20,7 @@
 
 from nautobot.dcim.models import Device
 from nautobot.core.views import generic
+
 from nautobot.utilities.utils import csv_format
 from nautobot.utilities.error_handlers import handle_protectederror
 from nautobot.utilities.views import ContentTypePermissionRequiredMixin
@@ -44,13 +46,17 @@ class GoldenConfigListView(generic.ObjectListView):
     table = tables.GoldenConfigTable
     filterset = filters.GoldenConfigFilter
     filterset_form = forms.GoldenConfigFilterForm
-    queryset = models.GoldenConfig.objects.all()
+    queryset = Device.objects.all()
     template_name = "nautobot_golden_config/goldenconfig_list.html"
 
     def extra_context(self):
         """Boilerplace code to modify data before returning."""
         return CONFIG_FEATURES
 
+    def alter_queryset(self, request):
+        """Build actual runtime queryset as the build time queryset provides no information."""
+        return self.queryset.filter(id__in=models.GoldenConfigSetting.objects.first().get_queryset())
+
 
 class GoldenConfigBulkDeleteView(generic.BulkDeleteView):
     """Standard view for bulk deletion of data."""
@@ -258,10 +264,24 @@ def get_required_permission(self):
         """Manually set permission when not tied to a model for config details."""
         return "nautobot_golden_config.view_goldenconfig"
 
-    def get(self, request, pk, config_type):  # pylint: disable=invalid-name,too-many-branches
+    def get(
+        self, request, pk, config_type
+    ):  # pylint: disable=invalid-name,too-many-branches,too-many-locals,too-many-statements
         """Read request into a view of a single device."""
+
+        def diff_structured_data(backup_data, intended_data):
+            """Utility function to provide `Unix Diff` between two JSON snippets."""
+            backup_yaml = yaml.safe_dump(json.loads(backup_data))
+            intend_yaml = yaml.safe_dump(json.loads(intended_data))
+
+            for line in difflib.unified_diff(backup_yaml.splitlines(), intend_yaml.splitlines(), lineterm=""):
+                yield line
+
         device = Device.objects.get(pk=pk)
         config_details = models.GoldenConfig.objects.filter(device=device).first()
+        if not config_details and config_type == "json_compliance":
+            # Create the GoldenConfig object for the device only for JSON compliance.
+            config_details = models.GoldenConfig.objects.create(device=device)
         structure_format = "json"
         if not config_details:
             output = ""
@@ -269,29 +289,63 @@ def get(self, request, pk, config_type):  # pylint: disable=invalid-name,too-man
             output = config_details.backup_config
         elif config_type == "intended":
             output = config_details.intended_config
-        elif config_type == "compliance":
-            output = config_details.compliance_config
-            if config_details.backup_last_success_date:
-                backup_date = str(config_details.backup_last_success_date.strftime("%b %d %Y"))
-            else:
-                backup_date = datetime.now().strftime("%b %d %Y")
-            if config_details.intended_last_success_date:
-                intended_date = str(config_details.intended_last_success_date.strftime("%b %d %Y"))
+        # Compliance type is broken up into JSON(json_compliance) and CLI(compliance) compliance.
+        elif "compliance" in config_type:
+            if config_type == "compliance":
+                # This section covers the steps to run regular CLI compliance which is a diff of 2 files (backup and intended).
+                diff_type = "File"
+                output = config_details.compliance_config
+                if config_details.backup_last_success_date:
+                    backup_date = str(config_details.backup_last_success_date.strftime("%b %d %Y"))
+                else:
+                    backup_date = datetime.now().strftime("%b %d %Y")
+                if config_details.intended_last_success_date:
+                    intended_date = str(config_details.intended_last_success_date.strftime("%b %d %Y"))
+                else:
+                    intended_date = datetime.now().strftime("%b %d %Y")
+            elif config_type == "json_compliance":
+                # The JSON compliance runs differently then CLI, it grabs all configcompliance objects for
+                # a given device and merges them, sorts them, and diffs them.
+                diff_type = "JSON"
+                # Get all compliance objects for a device.
+                compliance_objects = models.ConfigCompliance.objects.filter(device=device.id)
+                actual = {}
+                intended = {}
+                # Set a starting time that will be older than all last updated objects in compliance objects.
+                most_recent_time = datetime(1970, 1, 1, tzinfo=timezone.utc)
+                # Loop through config compliance objects and merge the data into one dataset.
+                for obj in compliance_objects:
+                    actual[obj.rule.feature.slug] = obj.actual
+                    intended[obj.rule.feature.slug] = obj.intended
+                    # Update most_recent_time each time the compliance objects time is more recent then previous.
+                    if obj.last_updated > most_recent_time:
+                        most_recent_time = obj.last_updated
+                config_details.compliance_last_attempt_date = most_recent_time
+                config_details.compliance_last_success_date = most_recent_time
+                # Generate the diff between both JSON objects and sort keys for accurate diff.
+                config_details.compliance_config = "\n".join(
+                    diff_structured_data(json.dumps(actual, sort_keys=True), json.dumps(intended, sort_keys=True))
+                )
+                config_details.save()
+                output = config_details.compliance_config
+                backup_date = intended_date = str(most_recent_time.strftime("%b %d %Y"))
+            if output == "":
+                # This is used if all config snippets are in compliance and no diff exist.
+                output = f"--- Backup {diff_type} - " + backup_date + f"\n+++ Intended {diff_type} - " + intended_date
             else:
-                intended_date = datetime.now().strftime("%b %d %Y")
-            first_occurence = output.index("@@")
-            second_occurence = output.index("@@", first_occurence)
-            # This is logic to match diff2html's expected input.
-            output = (
-                "--- Backup File - "
-                + backup_date
-                + "\n+++ Intended File - "
-                + intended_date
-                + "\n"
-                + output[first_occurence:second_occurence]
-                + "@@"
-                + output[second_occurence + 2 :]
-            )
+                first_occurence = output.index("@@")
+                second_occurence = output.index("@@", first_occurence)
+                # This is logic to match diff2html's expected input.
+                output = (
+                    f"--- Backup {diff_type} - "
+                    + backup_date
+                    + f"\n+++ Intended {diff_type} - "
+                    + intended_date
+                    + "\n"
+                    + output[first_occurence:second_occurence]
+                    + "@@"
+                    + output[second_occurence + 2 :]
+                )
         elif config_type == "sotagg":
             if request.GET.get("format") in ["json", "yaml"]:
                 structure_format = request.GET.get("format")
@@ -300,7 +354,7 @@ def get(self, request, pk, config_type):  # pylint: disable=invalid-name,too-man
             _, output = graph_ql_query(request, device, global_settings.sot_agg_query)
 
             if structure_format == "yaml":
-                output = yaml.dump(json.loads(json.dumps(output)), default_flow_style=False)
+                output = yaml.dump(output, default_flow_style=False)
             else:
                 output = json.dumps(output, indent=4)
 
diff --git a/poetry.lock b/poetry.lock
index 4faa37d0..86ee9e77 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -236,6 +236,20 @@ category = "main"
 optional = false
 python-versions = ">=3.6, <3.7"
 
+[[package]]
+name = "deepdiff"
+version = "5.5.0"
+description = "Deep Difference and Search of any Python object/data."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+ordered-set = "4.0.2"
+
+[package.extras]
+cli = ["click (==7.1.2)", "pyyaml (==5.4)", "toml (==0.10.2)", "clevercsv (==0.6.7)"]
+
 [[package]]
 name = "defusedxml"
 version = "0.7.1"
@@ -1088,6 +1102,14 @@ rsa = ["cryptography (>=3.0.0,<4)"]
 signals = ["blinker (>=1.4.0)"]
 signedtoken = ["cryptography (>=3.0.0,<4)", "pyjwt (>=2.0.0,<3)"]
 
+[[package]]
+name = "ordered-set"
+version = "4.0.2"
+description = "A set that remembers its order, and allows looking up its items by their index in that order."
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
 [[package]]
 name = "packaging"
 version = "21.0"
@@ -1760,7 +1782,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.6"
-content-hash = "66e595bea5b6e950a015d99b66e33041bfa2dd06a5a9837f04f92066684effd6"
+content-hash = "ab0bdabc106084605990ce202ba509c3d608fbe3d91bb23508451117a4decbe7"
 
 [metadata.files]
 aniso8601 = [
@@ -1896,6 +1918,10 @@ dataclasses = [
     {file = "dataclasses-0.7-py3-none-any.whl", hash = "sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836"},
     {file = "dataclasses-0.7.tar.gz", hash = "sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6"},
 ]
+deepdiff = [
+    {file = "deepdiff-5.5.0-py3-none-any.whl", hash = "sha256:e054fed9dfe0d83d622921cbb3a3d0b3a6dd76acd2b6955433a0a2d35147774a"},
+    {file = "deepdiff-5.5.0.tar.gz", hash = "sha256:dd79b81c2d84bfa33aa9d94d456b037b68daff6bb87b80dfaa1eca04da68b349"},
+]
 defusedxml = [
     {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
     {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
@@ -2332,6 +2358,9 @@ oauthlib = [
     {file = "oauthlib-3.1.1-py2.py3-none-any.whl", hash = "sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc"},
     {file = "oauthlib-3.1.1.tar.gz", hash = "sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"},
 ]
+ordered-set = [
+    {file = "ordered-set-4.0.2.tar.gz", hash = "sha256:ba93b2df055bca202116ec44b9bead3df33ea63a7d5827ff8e16738b97f33a95"},
+]
 packaging = [
     {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"},
     {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
@@ -2588,26 +2617,18 @@ pyyaml = [
     {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"},
     {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"},
     {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"},
-    {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"},
-    {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"},
     {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"},
     {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"},
     {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"},
     {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"},
-    {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"},
-    {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"},
     {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"},
     {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"},
     {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"},
     {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"},
-    {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"},
-    {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"},
     {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"},
     {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"},
     {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"},
     {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"},
-    {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"},
-    {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"},
     {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"},
     {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"},
     {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
@@ -2625,6 +2646,10 @@ regex = [
     {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa"},
     {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222"},
     {file = "regex-2021.7.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407"},
+    {file = "regex-2021.7.6-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf1d2d183abc7faa101ebe0b8d04fd19cb9138820abc8589083035c9440b8ca6"},
+    {file = "regex-2021.7.6-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1947e7de155063e1c495c50590229fb98720d4c383af5031bbcb413db33fa1be"},
+    {file = "regex-2021.7.6-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:17d8a3f99b18d87ac54a449b836d485cc8c195bb6f5e4379c84c8519045facc9"},
+    {file = "regex-2021.7.6-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d30895ec80cc80358392841add9dde81ea1d54a4949049269115e6b0555d0498"},
     {file = "regex-2021.7.6-cp36-cp36m-win32.whl", hash = "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b"},
     {file = "regex-2021.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb"},
     {file = "regex-2021.7.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d"},
@@ -2635,6 +2660,10 @@ regex = [
     {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a"},
     {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad"},
     {file = "regex-2021.7.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895"},
+    {file = "regex-2021.7.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8244c681018423a0d1784bc6b9af33bdf55f2ab8acb1f3cd9dd83d90e0813253"},
+    {file = "regex-2021.7.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a4c742089faf0e51469c6a1ad7e3d3d21afae54a16a6cead85209dfe0a1ce65"},
+    {file = "regex-2021.7.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:914e626dc8e75fe4fc9b7214763f141d9f40165d00dfe680b104fa1b24063bbf"},
+    {file = "regex-2021.7.6-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3fabb19c82ecf39832a3f5060dfea9a7ab270ef156039a1143a29a83a09a62de"},
     {file = "regex-2021.7.6-cp37-cp37m-win32.whl", hash = "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5"},
     {file = "regex-2021.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f"},
     {file = "regex-2021.7.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c"},
@@ -2645,6 +2674,10 @@ regex = [
     {file = "regex-2021.7.6-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd"},
     {file = "regex-2021.7.6-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761"},
     {file = "regex-2021.7.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068"},
+    {file = "regex-2021.7.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfc0957c4a4b91eff5ad036088769e600a25774256cd0e1154378591ce573f08"},
+    {file = "regex-2021.7.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:efb4af05fa4d2fc29766bf516f1f5098d6b5c3ed846fde980c18bf8646ad3979"},
+    {file = "regex-2021.7.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7423aca7cc30a6228ccdcf2ea76f12923d652c5c7c6dc1959a0b004e308f39fb"},
+    {file = "regex-2021.7.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb9834c1e77493efd7343b8e38950dee9797d2d6f2d5fd91c008dfaef64684b9"},
     {file = "regex-2021.7.6-cp38-cp38-win32.whl", hash = "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0"},
     {file = "regex-2021.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4"},
     {file = "regex-2021.7.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026"},
@@ -2655,6 +2688,10 @@ regex = [
     {file = "regex-2021.7.6-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d"},
     {file = "regex-2021.7.6-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58"},
     {file = "regex-2021.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3"},
+    {file = "regex-2021.7.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598ee917dbe961dcf827217bf2466bb86e4ee5a8559705af57cbabb3489dd37e"},
+    {file = "regex-2021.7.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:56fc7045a1999a8d9dd1896715bc5c802dfec5b9b60e883d2cbdecb42adedea4"},
+    {file = "regex-2021.7.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8363ac90ea63c3dd0872dfdb695f38aff3334bfa5712cffb238bd3ffef300e3"},
+    {file = "regex-2021.7.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:716a6db91b3641f566531ffcc03ceec00b2447f0db9942b3c6ea5d2827ad6be3"},
     {file = "regex-2021.7.6-cp39-cp39-win32.whl", hash = "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035"},
     {file = "regex-2021.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c"},
     {file = "regex-2021.7.6.tar.gz", hash = "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d"},
diff --git a/pyproject.toml b/pyproject.toml
index cbe01698..9a7dcf9f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -35,6 +35,7 @@ nautobot-plugin-nornir = ">=0.9.3"
 django-debug-toolbar = "^3.2.1"
 astroid = "^2.5.5"
 django-pivot = "^1.8.1"
+deepdiff = "^5.5.0"
 
 [tool.poetry.dev-dependencies]
 black = "^20.8b1"


Test patch:
diff --git a/nautobot_golden_config/tests/conftest.py b/nautobot_golden_config/tests/conftest.py
new file mode 100644
index 00000000..28622a54
--- /dev/null
+++ b/nautobot_golden_config/tests/conftest.py
@@ -0,0 +1,45 @@
+"""Params for testing."""
+from nautobot.dcim.models import Device, Site, Manufacturer, DeviceType, DeviceRole, Region, Platform
+
+from nautobot_golden_config.models import ConfigCompliance, ComplianceFeature, ComplianceRule
+from nautobot_golden_config.choices import ComplianceRuleTypeChoice
+
+
+def create_device(name="foobaz"):
+    """Creates a Device to be used with tests."""
+    parent_region, _ = Region.objects.get_or_create(name="Parent Region", slug="parent_region")
+    child_region, _ = Region.objects.get_or_create(name="Child Region", slug="child_region", parent=parent_region)
+    site, _ = Site.objects.get_or_create(name="foo", slug="foo", region=child_region)
+    manufacturer, _ = Manufacturer.objects.get_or_create(name="bar")
+    device_role, _ = DeviceRole.objects.get_or_create(name="baz")
+    device_type, _ = DeviceType.objects.get_or_create(manufacturer=manufacturer)
+    platform, _ = Platform.objects.get_or_create(manufacturer=manufacturer)
+    device = Device.objects.create(
+        name=name, platform=platform, site=site, device_role=device_role, device_type=device_type
+    )
+    return device
+
+
+def create_feature_rule_json(device, feature="foo", rule="json"):
+    """Creates a Feature/Rule Mapping and Returns the rule."""
+    feature_obj = ComplianceFeature(slug=feature, name=feature)
+    feature_obj.save()
+    rule = ComplianceRule(
+        feature=feature_obj,
+        platform=device.platform,
+        config_type=ComplianceRuleTypeChoice.TYPE_JSON,
+        config_ordered=False,
+    )
+    rule.save()
+    return rule
+
+
+def create_config_compliance(device, compliance_rule=None, actual=None, intended=None):
+    """Creates a ConfigCompliance to be used with tests."""
+    config_compliance = ConfigCompliance.objects.create(
+        device=device,
+        rule=compliance_rule,
+        actual=actual,
+        intended=intended,
+    )
+    return config_compliance
diff --git a/nautobot_golden_config/tests/test_api.py b/nautobot_golden_config/tests/test_api.py
index deefcd21..ff744ec2 100644
--- a/nautobot_golden_config/tests/test_api.py
+++ b/nautobot_golden_config/tests/test_api.py
@@ -6,6 +6,8 @@
 
 from nautobot.utilities.testing import APITestCase
 
+from .conftest import create_device, create_feature_rule_json, create_config_compliance
+
 
 User = get_user_model()
 
@@ -13,10 +15,69 @@
 class GoldenConfigAPITest(APITestCase):  # pylint: disable=too-many-ancestors
     """Test the ConfigCompliance API."""
 
+    def setUp(self):
+        """Create a superuser and token for API calls."""
+        super().setUp()
+        self.device = create_device()
+        self.compliance_rule_json = create_feature_rule_json(self.device)
+        self.base_view = reverse("plugins-api:nautobot_golden_config-api:configcompliance-list")
+
+    def test_root(self):
+        """Validate the root for Nautobot Chatops API."""
+        url = reverse("plugins-api:nautobot_golden_config-api:api-root")
+        response = self.client.get("{}?format=api".format(url), **self.header)
+        self.assertEqual(response.status_code, 200)
+
     def test_device_list(self):
         """Verify that devices can be listed."""
         url = reverse("dcim-api:device-list")
         self.add_permissions("dcim.view_device")
         response = self.client.get(url, **self.header)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertEqual(response.data["count"], 0)
+        self.assertEqual(response.data["count"], 1)
+
+    def test_config_compliance_list_view(self):
+        """Verify that config compliance objects can be listed."""
+        actual = '{"foo": {"bar-1": "baz"}}'
+        intended = '{"foo": {"bar-2": "baz"}}'
+        create_config_compliance(
+            self.device, actual=actual, intended=intended, compliance_rule=self.compliance_rule_json
+        )
+        self.add_permissions("nautobot_golden_config.view_configcompliance")
+        response = self.client.get(self.base_view, **self.header)
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data["count"], 1)
+
+    def test_config_compliance_post_new_json_compliant(self):
+        """Verify that config compliance detail view."""
+        self.add_permissions("nautobot_golden_config.add_configcompliance")
+        response = self.client.post(
+            self.base_view,
+            data={
+                "device": self.device.id,
+                "intended": '{"foo": {"bar-1": "baz"}}',
+                "actual": '{"foo": {"bar-1": "baz"}}',
+                "rule": self.compliance_rule_json.id,
+            },
+            format="json",
+            **self.header,
+        )
+        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+        self.assertTrue(response.data["compliance"])
+
+    def test_config_compliance_post_new_json_not_compliant(self):
+        """Verify that config compliance detail view."""
+        self.add_permissions("nautobot_golden_config.add_configcompliance")
+        response = self.client.post(
+            self.base_view,
+            data={
+                "device": self.device.id,
+                "intended": '{"foo": {"bar-1": "baz"}}',
+                "actual": '{"foo": {"bar-2": "baz"}}',
+                "rule": self.compliance_rule_json.id,
+            },
+            format="json",
+            **self.header,
+        )
+        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+        self.assertFalse(response.data["compliance"])
diff --git a/nautobot_golden_config/tests/test_models.py b/nautobot_golden_config/tests/test_models.py
index d99407b2..f315baa1 100644
--- a/nautobot_golden_config/tests/test_models.py
+++ b/nautobot_golden_config/tests/test_models.py
@@ -2,18 +2,74 @@
 
 from django.test import TestCase
 from django.core.exceptions import ValidationError
-
+from django.db.utils import IntegrityError
 from nautobot.dcim.models import Platform
 
 from nautobot_golden_config.models import (
+    ConfigCompliance,
     GoldenConfigSetting,
     ConfigRemove,
     ConfigReplace,
 )
 
+from .conftest import create_device, create_feature_rule_json, create_config_compliance
+
 
 class ConfigComplianceModelTestCase(TestCase):
-    """Test ConfigCompliance Model."""
+    """Test CRUD operations for ConfigCompliance Model."""
+
+    def setUp(self):
+        """Set up base objects."""
+        self.device = create_device()
+        self.compliance_rule_json = create_feature_rule_json(self.device)
+
+    def test_create_config_compliance_success_json(self):
+        """Successful."""
+        actual = {"foo": {"bar-1": "baz"}}
+        intended = {"foo": {"bar-2": "baz"}}
+        cc_obj = create_config_compliance(
+            self.device, actual=actual, intended=intended, compliance_rule=self.compliance_rule_json
+        )
+
+        self.assertFalse(cc_obj.compliance)
+        self.assertEqual(cc_obj.actual, {"foo": {"bar-1": "baz"}})
+        self.assertEqual(cc_obj.intended, {"foo": {"bar-2": "baz"}})
+        self.assertEqual(cc_obj.missing, ["root['foo']['bar-2']"])
+        self.assertEqual(cc_obj.extra, ["root['foo']['bar-1']"])
+
+    def test_create_config_compliance_unique_failure(self):
+        """Raises error when attempting to create duplicate."""
+        ConfigCompliance.objects.create(
+            device=self.device,
+            rule=self.compliance_rule_json,
+            actual={"foo": {"bar-1": "baz"}},
+            intended={"foo": {"bar-2": "baz"}},
+            missing={},
+            extra={},
+        )
+        with self.assertRaises(IntegrityError):
+            ConfigCompliance.objects.create(
+                device=self.device,
+                rule=self.compliance_rule_json,
+                compliance=False,
+                actual={"foo": {"bar-1": "baz"}},
+                intended={"foo": {"bar-2": "baz"}},
+                missing={},
+                extra={},
+            )
+
+    def test_create_config_compliance_success_compliant(self):
+        """Successful."""
+        cc_obj = ConfigCompliance.objects.create(
+            device=self.device,
+            rule=self.compliance_rule_json,
+            actual={"foo": {"bar-1": "baz"}},
+            intended={"foo": {"bar-1": "baz"}},
+        )
+
+        self.assertTrue(cc_obj.compliance)
+        self.assertEqual(cc_obj.missing, "")
+        self.assertEqual(cc_obj.extra, "")
 
 
 class GoldenConfigTestCase(TestCase):
diff --git a/nautobot_golden_config/tests/test_nornir_plays/test_config_compliance.py b/nautobot_golden_config/tests/test_nornir_plays/test_config_compliance.py
index c1511fee..d6989b2f 100644
--- a/nautobot_golden_config/tests/test_nornir_plays/test_config_compliance.py
+++ b/nautobot_golden_config/tests/test_nornir_plays/test_config_compliance.py
@@ -15,9 +15,9 @@ def test_get_features(self, mock_compliance_rule):
         mock_obj = Mock(**features)
         mock_obj.name = "test_name"
         mock_obj.platform = Mock(slug="test_slug")
-        mock_compliance_rule.objects.all.return_value = [mock_obj]
+        mock_compliance_rule.objects.filter.return_value = [mock_obj]
         features = get_features()
-        mock_compliance_rule.objects.all.assert_called_once()
+        mock_compliance_rule.objects.filter.assert_called_once()
         self.assertEqual(
             features, {"test_slug": [{"obj": mock_obj, "ordered": "test_ordered", "section": ["aaa", "snmp"]}]}
         )