docs: function diffObjectProps(
  	container, pathPrefix, objOld, objNew, unrecognizedPropNames, patchSpec) {
  
  	// check each property
  	for (let propName of container.allPropertyNames) {
  
  		// property recognized
  		unrecognizedPropNames.delete(propName);
  
  		// get property descriptor
  		const propDesc = container.getPropertyDesc(propName);
  
  		// skip certain types of properties
  		if (propDesc.isView() || propDesc.isCalculated() ||
  			propDesc.isRecordMetaInfo() || propDesc.isSubtype())
  			continue;
  
  		// get the property values, old and new
  		const valOld = objOld[propName];
  		const valNew = objNew[propName];
  
  		// common check for property removal
  		if ((valNew === undefined) || (valNew === null)) {
  			if ((valOld !== undefined) && (valOld !== null) &&
  				!propDesc.isId())
  				patchSpec.push({
  					op: 'remove',
  					path: `${pathPrefix}${propName}`
  				});
  			continue;
  		}
  
  		// proceed depending on the property structural type
  		if (propDesc.isArray()) {
  
  			if (!Array.isArray(valNew))
  				throw new common.X2SyntaxError(
  					`Provided value for ${container.recordTypeName} property` +
  					` at ${pathPrefix}${propName} is not an array.`);
  
  			if (valNew.length === 0) {
  				if ((valOld !== undefined) && (valOld !== null) &&
  					(valOld.length > 0))
  					patchSpec.push({
  						op: 'remove',
  						path: `${pathPrefix}${propName}`
  					});
  			} else if ((valOld === undefined) || (valOld === null) ||
  						(valOld.length === 0)) {
  				patchSpec.push({
  					op: 'replace',
  					path: `${pathPrefix}${propName}`,
  					value: valNew
  				});
  			} else {
  				if (propDesc.scalarValueType === 'object')
  					diffObjectArrays(
  						propDesc, `${pathPrefix}${propName}`, valOld, valNew,
  						patchSpec);
  				else
  					diffValueArrays(
  						`${pathPrefix}${propName}`, valOld, valNew, patchSpec);
  			}
  
  		} else if (propDesc.isMap()) {
  
  			if ((typeof valNew) !== 'object')
  				throw new common.X2SyntaxError(
  					`Provided value for ${container.recordTypeName} property` +
  					` at ${pathPrefix}${propName} is not an object.`);
  
  			if (Object.keys(valNew).length === 0) {
  				if ((valOld !== undefined) && (valOld !== null) &&
  					(Object.keys(valOld).length > 0))
  					patchSpec.push({
  						op: 'remove',
  						path: `${pathPrefix}${propName}`
  					});
  			} else if ((valOld === undefined) || (valOld === null) ||
  						(Object.keys(valOld).length === 0)) {
  				patchSpec.push({
  					op: 'replace',
  					path: `${pathPrefix}${propName}`,
  					value: valNew
  				});
  			} else {
  				diffMaps(
  					propDesc, `${pathPrefix}${propName}`, valOld, valNew,
  					patchSpec);
  			}
  
  		} else if (propDesc.scalarValueType === 'object') {
  
  			if ((typeof valNew) !== 'object')
  				throw new common.X2SyntaxError(
  					`Provided value for ${container.recordTypeName} property` +
  					` at ${pathPrefix}${propName} is not an object.`);
  
  			if ((valOld === undefined) || (valOld === null)) {
  				patchSpec.push({
  					op: 'replace',
  					path: `${pathPrefix}${propName}`,
  					value: valNew
  				});
  			} else {
  				diffObjects(
  					propDesc.nestedProperties, `${pathPrefix}${propName}/`,
  					valOld, valNew, patchSpec);
  			}
  
  		} else {
  
  			if (valNew !== valOld)
  				patchSpec.push({
  					op: 'replace',
  					path: `${pathPrefix}${propName}`,
  					value: valNew
  				});
  		}
  	}
  }