# Json compare tool for Views and Tags?

**URL:** <https://forum.inductiveautomation.com/t/json-compare-tool-for-views-and-tags/41163>\
**Category:** Ignition\
**Tags:** ignition80, scripting\
**Created:** [December 3, 2020, 4:56am UTC](https://forum.inductiveautomation.com/t/json-compare-tool-for-views-and-tags/41163 "2020-12-03T04:56:30Z")\
**Posts on this page:** 1\
**Showing post:** 5

<div class="post-metadata">

**Author:** ![nminchin](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/nminchin/32/6911_2.png) [@nminchin](https://forum.inductiveautomation.com/u/nminchin)\
**Post date:** [November 13, 2023, 8:26pm UTC](https://forum.inductiveautomation.com/t/json-compare-tool-for-views-and-tags/41163/5 "2023-11-13T20:26:25Z")

</div>

I recently rewrote my tag compare script which was a bit dopey, to use a Python library `littletree` I found via StackOverflow. Most of the kudos goes to its developer who answered my [SO question](https://stackoverflow.com/questions/77443116/how-to-compare-two-json-files-that-adhere-to-a-schema/77463485?noredirect=1#comment136580884_77463485) with the majority of the code, but I modified it to include more detail and spit out a CSV.

This will only run in CPython, I use PyCharm as my IDE. It requires a min of Python 3.10 (dependency of the `littletree` library).  
The only library dependency not included in the standard libraries is `littletree` (`pip install littletree`)

```python
from littletree import Node # littletree requires minimum Python 3.10
import json

def compare_tag_json(json_orig_filepath, json_new_filepath, exclude_tags_in_removed_folders=True, exclude_tags_in_added_udt_instances=True):
	"""
	This is a wrapper for function compare_tag_obj.
	Takes two tag JSON exports to files and produces a report of changes between the tag structures, including:
		- additions/deletions of tags
		- additions/modifications/removal of tag props
	
	Args:
		json_orig_filepath:
			The filepath to the original tag json file for the comparison.
		json_new_filepath:
			The filepath to the new tag json file for the comparison.
		exclude_tags_in_removed_folders:
			For folders removed in the new file, this option will exclude the tag changes (i.e. deletions) to the tags
			contained within the folder. 
			Default: True
		exclude_tags_in_added_udt_instances:
			For UDT Instances added in the new file, this option will exclude the tag changes (i.e. additions) to the 
			tags contained within the UDT Instance.
			Default: True
	
	Returns:
		The table of changes in CSV format with headers: tagpath, tagtype, change, from, to 
		Also prints the results as a CSV to the console.
	"""
	with open(json_orig_filepath, 'r') as f:
		json_orig = f.read()

	with open(json_new_filepath, 'r') as f:
		json_new = f.read()

	original = json.loads(json_orig)
	modified = json.loads(json_new)

	return compare_tag_obj(original, modified, exclude_tags_in_removed_folders, exclude_tags_in_added_udt_instances)

def compare_tag_obj(tags_original, tags_modified, exclude_tags_in_removed_folders=True, exclude_tags_in_added_udt_instances=True):
	"""
	Takes two tag JSON as Py lists/dicts and produces a report of changes between the tag structures, including:
		- additions/deletions of tags
		- additions/modifications/removal of tag props
	
	Args:
		tags_original:
			The list/dict structure of the original tag json file for the comparison.
		tags_modified:
			The list/dict structure of the new tag json file for the comparison.
		exclude_tags_in_removed_folders:
			For folders removed in the new file, this option will exclude the tag changes (i.e. deletions) to the tags
			contained within the folder. 
			Default: True
		exclude_tags_in_added_udt_instances:
			For UDT Instances added in the new file, this option will exclude the tag changes (i.e. additions) to the 
			tags contained within the UDT Instance.
			Default: True
	
	Returns:
		The table of changes in CSV format with headers: tagpath, tagtype, change, from, to 
		Also prints the results as a CSV to the console.
	"""
	original_tree = Node.from_dict(tags_original, identifier_name='name', children_name='tags')
	modified_tree = Node.from_dict(tags_modified, identifier_name='name', children_name='tags')

	# Collect changes in a list
	changes = []
	for diff_node in original_tree.compare(modified_tree).iter_tree():
		diff_data = diff_node.data
		tagpath = str(diff_node.path)[1:]
		if not diff_data:
			continue # Data was the same

		if 'self' not in diff_data:
			if not exclude_tags_in_added_udt_instances or not any(change['tagpath'] in tagpath and change['change'] == 'Tag added' and change['tagtype'] == 'UdtInstance' for change in changes):
				changes.append({'tagpath': tagpath,
								'tagtype': diff_data['other']['tagType'],
								'change': 'Tag added',
								'from': None,
								'to': None})
		elif 'other' not in diff_data:
			if not exclude_tags_in_removed_folders or not any(change['tagpath'] in tagpath and change['change'] == 'Tag removed' for change in changes):
				changes.append({'tagpath': tagpath,
								'tagtype': diff_data['self']['tagType'],
								'change': 'Tag removed',
								'from': None,
								'to': None})
		else:
			original_data, modified_data = diff_data['self'], diff_data['other']
			all_keys = set(list(original_data.keys()) + list(modified_data.keys()))

			for key in all_keys:
				if key in original_data and key in modified_data:
					if original_data[key] != modified_data[key]:
						changes.append({'tagpath': f'{tagpath}.{key}',
										'tagtype': original_data['tagType'],
										'change': 'Modified property',
										'from': original_data[key],
										'to': modified_data[key]
										})
				elif key in original_data:
					changes.append({'tagpath': f'{tagpath}.{key}',
									'tagtype': original_data['tagType'],
									'change': 'Removed property',
									'from': original_data[key],
									'to': None
									})
				elif key in modified_data:
					changes.append({'tagpath': f'{tagpath}.{key}',
									'tagtype': modified_data['tagType'],
									'change': 'Added property',
									'from': None,
									'to': modified_data[key]})

	changes_csv = '"' + '","'.join(changes[0].keys()) + '"\r\n'
	changes_csv += '\r\n'.join('"' + '","'.join(map(str, change.values())) + '"' for change in changes)
	print(changes_csv)
	return changes_csv

```

The result is a table like this:

| tagpath | tagtype | change | from | to |
| --- | --- | --- | --- | --- |
| A/New Tag 1.value | AtomicTag | Added property | None | 1 |
| A/New Instance | UdtInstance | Tag removed | None | None |
| A/New Tag 2 | AtomicTag | Tag removed | None | None |
| A/New Tag.value | AtomicTag | Modified property | 2 | 1 |
| A/Folder B | Folder | Tag added | None | None |
| A/New Instance 3 | UdtInstance | Tag added | None | None |

or this:

| tagpath | tagtype | change | from | to |
| --- | --- | --- | --- | --- |
| Analogue Input 01.parameters | UdtType | Modified property | {'Alarm\_ParentDevice': {'dataType': 'String'}, 'Unit': {'dataType': 'String', 'value': ''}, 'Alarm\_Area': {'dataType': 'String'}, 'Format': {'dataType': 'String', 'value': '#,##0.0'}, 'PLCName': {'dataType': 'String', 'value': {'bindType': 'parameter', 'binding': '{PLCName}'}}, 'Description': {'dataType': 'String', 'value': ''}, 'DeviceName': {'dataType': 'String', 'value': ''}, 'Global.': {'dataType': 'String', 'value': ''}} | {'Alarm\_ParentDevice': {'dataType': 'String'}, 'Unit': {'dataType': 'String'}, 'Alarm\_Area': {'dataType': 'String'}, 'Format': {'dataType': 'String', 'value': '#,##0.0'}, 'PLCName': {'dataType': 'String', 'value': {'bindType': 'parameter', 'binding': '{PLCName}'}}, 'Description': {'dataType': 'String', 'value': ''}, 'DeviceName': {'dataType': 'String', 'value': ''}, 'Global.': {'dataType': 'String', 'value': ''}} |
| Analogue Input 01/Low Alarm SP | AtomicTag | Tag removed | None | None |
| Analogue Input 01/Alarms/High | AtomicTag | Tag removed | None | None |
| Analogue Input 01/Alarms/\_AnyActive.executionMode | AtomicTag | Removed property | EventDriven | None |
| Analogue Input 01/Alarms/\_AnyActive.expression | AtomicTag | Removed property | {[.]High High} | |
| Analogue Input 01/Alarms/\_AnyActive.opcItemPath | AtomicTag | Modified property | {'bindType': 'parameter', 'binding': 'ns=1;s=[{PLCName}]{Global.}{DeviceName}.Alarms'} | {'bindType': 'parameter', 'binding': 'ns=1;s=[{PLCName}]{Global.}{DeviceName}.AnyAlarm'} |
| Analogue Input 01/Alarms/Low Low | AtomicTag | Tag removed | None | None |
| Analogue Input 01/Alarms/Low | AtomicTag | Tag removed | None | None |
| Analogue Input 01/Alarms/Comms Fault | AtomicTag | Tag removed | None | None |
| Analogue Input 01/Alarms/Signal Fault | AtomicTag | Tag added | None | None |
| Analogue Input 01/Alarms/Over or Under Range | AtomicTag | Tag added | None | None |
| Analogue Input 01/Simulation | AtomicTag | Tag removed | None | None |
| Analogue Input 01/High Alarm SP | AtomicTag | Tag removed | None | None |
| Analogue Input 01/Low Low Alarm SP | AtomicTag | Tag removed | None | None |
| Analogue Input 01/Low Alarm Time | AtomicTag | Tag removed | None | None |
| Analogue Input 01/High High Alarm Time | AtomicTag | Tag removed | None | None |
| Analogue Input 01/High Alarm Time | AtomicTag | Tag removed | None | None |
| Analogue Input 01/Low Low Alarm Time | AtomicTag | Tag removed | None | None |
| Analogue Input 01/High High Alarm SP | AtomicTag | Tag removed | None | None |

> **Test Code**
>
> ```python
> original = {
> 'name': 'A',
> 'tagType': 'Folder',
> 'tags': [
> {
> 'valueSource': 'memory',
> 'name': 'New Tag 1',
> 'tagType': 'AtomicTag'
> },
> {
> 'name': 'New Instance',
> 'typeId': 'Alarm Summary',
> 'parameters': {
> 'MainTagPath': {
> 'dataType': 'String',
> 'value': 'Bob'
> },
> 'Description': {
> 'dataType': 'String',
> 'value': 'Descrp'
> }
> },
> 'tagType': 'UdtInstance',
> 'tags': [
> {
> 'name': 'Area PLCs for Alarm Reset',
> 'tagType': 'AtomicTag'
> },
> {
> 'name': 'Active Ack',
> 'tagType': 'AtomicTag'
> },
> {
> 'name': 'Active Unack',
> 'tagType': 'AtomicTag'
> },
> {
> 'name': 'Clear Unack',
> 'tagType': 'AtomicTag'
> }
> ]
> },
> {
> 'valueSource': 'memory',
> 'name': 'New Tag 2',
> 'tagType': 'AtomicTag'
> },
> {
> 'name': 'New Instance 1',
> 'typeId': 'Alarm Summary',
> 'parameters': {
> 'MainTagPath': {
> 'dataType': 'String',
> 'value': 'Bob'
> },
> 'Description': {
> 'dataType': 'String',
> 'value': 'Descrp'
> }
> },
> 'tagType': 'UdtInstance',
> 'tags': [
> {
> 'name': 'Active Ack',
> 'tagType': 'AtomicTag'
> },
> {
> 'name': 'Area PLCs for Alarm Reset',
> 'tagType': 'AtomicTag'
> },
> {
> 'name': 'Clear Unack',
> 'tagType': 'AtomicTag'
> },
> {
> 'name': 'Active Unack',
> 'tagType': 'AtomicTag'
> }
> ]
> },
> {
> 'valueSource': 'memory',
> 'name': 'New Tag',
> 'value': 2,
> 'tagType': 'AtomicTag'
> }
> ]
> }
> 
> modified = {
> 'name': 'A',
> 'tagType': 'Folder',
> 'tags': [
> {'name': 'Folder B',
> 'tagType': 'Folder'
> },
> {
> 'valueSource': 'memory',
> 'name': 'New Tag',
> 'value': 1,
> 'tagType': 'AtomicTag'
> },
> {
> 'valueSource': 'memory',
> 'name': 'New Tag 1',
> 'value': 1,
> 'tagType': 'AtomicTag'
> },
> {
> 'name': 'New Instance 3',
> 'typeId': 'Alarm Summary',
> 'parameters': {
> 'MainTagPath': {
> 'dataType': 'String',
> 'value': 'Bob'
> },
> 'Description': {
> 'dataType': 'String',
> 'value': 'Descrp'
> }
> },
> 'tagType': 'UdtInstance',
> 'tags': [
> {
> 'name': 'Area PLCs for Alarm Reset',
> 'tagType': 'AtomicTag'
> },
> {
> 'name': 'Clear Unack',
> 'tagType': 'AtomicTag'
> },
> {
> 'name': 'Active Unack',
> 'tagType': 'AtomicTag'
> },
> {
> 'name': 'Active Ack',
> 'tagType': 'AtomicTag'
> }
> ]
> }
> ]
> }
> 
> compare_tag_obj(original, modified, exclude_tags_in_removed_folders=True, exclude_tags_in_added_udt_instances=True)
> 
> ```

---

_[View the full topic](https://forum.inductiveautomation.com/t/json-compare-tool-for-views-and-tags/41163)._
