The Merge Algorithm
internal static class SchemaVersionMerger
{
public static UnifiedSchema Merge(List<SchemaModel> schemas)
{
if (schemas.Count == 0)
return new UnifiedSchema();
// Step 1: Sort by semantic version
schemas.Sort((a, b) => CompareVersions(a.Version, b.Version));
var unified = new UnifiedSchema
{
Versions = schemas.Select(s => s.Version).ToList()
};
var firstVersion = schemas[0].Version;
var lastVersion = schemas[schemas.Count - 1].Version;
// Step 2: Merge definitions across versions
var allDefNames = schemas
.SelectMany(s => s.Definitions.Keys)
.Distinct().ToList();
foreach (var defName in allDefNames)
{
var firstAppearance = schemas
.FirstOrDefault(s => s.Definitions.ContainsKey(defName));
var lastAppearance = schemas
.LastOrDefault(s => s.Definitions.ContainsKey(defName));
if (firstAppearance is null) continue;
// Use latest version's definition (most complete)
var latestDef = schemas
.Last(s => s.Definitions.ContainsKey(defName))
.Definitions[defName];
var uniDef = new UnifiedDefinition
{
Name = defName,
Description = latestDef.Description,
SinceVersion = firstAppearance.Version == firstVersion
? null : firstAppearance.Version,
UntilVersion = lastAppearance!.Version == lastVersion
? null : lastAppearance.Version
};
// Step 3: Merge properties within each definition
var allPropNames = schemas
.Where(s => s.Definitions.ContainsKey(defName))
.SelectMany(s => s.Definitions[defName].Properties
.Select(p => p.JsonName))
.Distinct().ToList();
foreach (var propName in allPropNames)
{
var firstPropVersion = schemas.FirstOrDefault(s =>
s.Definitions.ContainsKey(defName) &&
s.Definitions[defName].Properties
.Any(p => p.JsonName == propName));
var lastPropVersion = schemas.LastOrDefault(s =>
s.Definitions.ContainsKey(defName) &&
s.Definitions[defName].Properties
.Any(p => p.JsonName == propName));
if (firstPropVersion is null) continue;
// Use latest property definition
var latestProp = schemas
.Last(s => s.Definitions.ContainsKey(defName) &&
s.Definitions[defName].Properties
.Any(p => p.JsonName == propName))
.Definitions[defName].Properties
.First(p => p.JsonName == propName);
uniDef.Properties.Add(new UnifiedProperty
{
Property = latestProp,
SinceVersion = firstPropVersion.Version == firstVersion
? null : firstPropVersion.Version,
UntilVersion = lastPropVersion!.Version == lastVersion
? null : lastPropVersion.Version
});
}
unified.Definitions[defName] = uniDef;
}
// Step 4: Merge root properties (same algorithm)
var allRootProps = schemas
.SelectMany(s => s.RootProperties.Select(p => p.JsonName))
.Distinct().ToList();
foreach (var propName in allRootProps)
{
var firstPropVersion = schemas
.FirstOrDefault(s => s.RootProperties
.Any(p => p.JsonName == propName));
var lastPropVersion = schemas
.LastOrDefault(s => s.RootProperties
.Any(p => p.JsonName == propName));
if (firstPropVersion is null) continue;
var latestProp = schemas
.Last(s => s.RootProperties.Any(p => p.JsonName == propName))
.RootProperties.First(p => p.JsonName == propName);
unified.RootProperties.Add(new UnifiedProperty
{
Property = latestProp,
SinceVersion = firstPropVersion.Version == firstVersion
? null : firstPropVersion.Version,
UntilVersion = lastPropVersion!.Version == lastVersion
? null : lastPropVersion.Version
});
}
return unified;
}
}internal static class SchemaVersionMerger
{
public static UnifiedSchema Merge(List<SchemaModel> schemas)
{
if (schemas.Count == 0)
return new UnifiedSchema();
// Step 1: Sort by semantic version
schemas.Sort((a, b) => CompareVersions(a.Version, b.Version));
var unified = new UnifiedSchema
{
Versions = schemas.Select(s => s.Version).ToList()
};
var firstVersion = schemas[0].Version;
var lastVersion = schemas[schemas.Count - 1].Version;
// Step 2: Merge definitions across versions
var allDefNames = schemas
.SelectMany(s => s.Definitions.Keys)
.Distinct().ToList();
foreach (var defName in allDefNames)
{
var firstAppearance = schemas
.FirstOrDefault(s => s.Definitions.ContainsKey(defName));
var lastAppearance = schemas
.LastOrDefault(s => s.Definitions.ContainsKey(defName));
if (firstAppearance is null) continue;
// Use latest version's definition (most complete)
var latestDef = schemas
.Last(s => s.Definitions.ContainsKey(defName))
.Definitions[defName];
var uniDef = new UnifiedDefinition
{
Name = defName,
Description = latestDef.Description,
SinceVersion = firstAppearance.Version == firstVersion
? null : firstAppearance.Version,
UntilVersion = lastAppearance!.Version == lastVersion
? null : lastAppearance.Version
};
// Step 3: Merge properties within each definition
var allPropNames = schemas
.Where(s => s.Definitions.ContainsKey(defName))
.SelectMany(s => s.Definitions[defName].Properties
.Select(p => p.JsonName))
.Distinct().ToList();
foreach (var propName in allPropNames)
{
var firstPropVersion = schemas.FirstOrDefault(s =>
s.Definitions.ContainsKey(defName) &&
s.Definitions[defName].Properties
.Any(p => p.JsonName == propName));
var lastPropVersion = schemas.LastOrDefault(s =>
s.Definitions.ContainsKey(defName) &&
s.Definitions[defName].Properties
.Any(p => p.JsonName == propName));
if (firstPropVersion is null) continue;
// Use latest property definition
var latestProp = schemas
.Last(s => s.Definitions.ContainsKey(defName) &&
s.Definitions[defName].Properties
.Any(p => p.JsonName == propName))
.Definitions[defName].Properties
.First(p => p.JsonName == propName);
uniDef.Properties.Add(new UnifiedProperty
{
Property = latestProp,
SinceVersion = firstPropVersion.Version == firstVersion
? null : firstPropVersion.Version,
UntilVersion = lastPropVersion!.Version == lastVersion
? null : lastPropVersion.Version
});
}
unified.Definitions[defName] = uniDef;
}
// Step 4: Merge root properties (same algorithm)
var allRootProps = schemas
.SelectMany(s => s.RootProperties.Select(p => p.JsonName))
.Distinct().ToList();
foreach (var propName in allRootProps)
{
var firstPropVersion = schemas
.FirstOrDefault(s => s.RootProperties
.Any(p => p.JsonName == propName));
var lastPropVersion = schemas
.LastOrDefault(s => s.RootProperties
.Any(p => p.JsonName == propName));
if (firstPropVersion is null) continue;
var latestProp = schemas
.Last(s => s.RootProperties.Any(p => p.JsonName == propName))
.RootProperties.First(p => p.JsonName == propName);
unified.RootProperties.Add(new UnifiedProperty
{
Property = latestProp,
SinceVersion = firstPropVersion.Version == firstVersion
? null : firstPropVersion.Version,
UntilVersion = lastPropVersion!.Version == lastVersion
? null : lastPropVersion.Version
});
}
return unified;
}
}Version Tracking Logic
The merger assigns version metadata based on three rules:
SinceVersion = nullwhen a property exists in the first schema version (18.0.0). This means "always available" — no annotation needed.SinceVersion = "X.Y.Z"when a property first appears in a later version. For example,Inputson jobs first appears in 18.6.0, so it gets[SinceVersion("18.6.0")].UntilVersion = "X.Y.Z"when a property disappears before the latest version. This is rare but handled for completeness.
Version Comparison
internal static int CompareVersions(string a, string b)
{
var partsA = a.Split('.');
var partsB = b.Split('.');
for (var i = 0; i < System.Math.Max(partsA.Length, partsB.Length); i++)
{
var va = i < partsA.Length && int.TryParse(partsA[i], out var ia) ? ia : 0;
var vb = i < partsB.Length && int.TryParse(partsB[i], out var ib) ? ib : 0;
if (va != vb) return va.CompareTo(vb);
}
return 0;
}internal static int CompareVersions(string a, string b)
{
var partsA = a.Split('.');
var partsB = b.Split('.');
for (var i = 0; i < System.Math.Max(partsA.Length, partsB.Length); i++)
{
var va = i < partsA.Length && int.TryParse(partsA[i], out var ia) ? ia : 0;
var vb = i < partsB.Length && int.TryParse(partsB[i], out var ib) ? ib : 0;
if (va != vb) return va.CompareTo(vb);
}
return 0;
}Unified Schema Model
The output of the merger:
internal sealed class UnifiedSchema
{
public List<string> Versions { get; set; } = new();
public Dictionary<string, UnifiedDefinition> Definitions { get; set; } = new();
public List<UnifiedProperty> RootProperties { get; set; } = new();
}
internal sealed class UnifiedDefinition
{
public string Name { get; set; } = "";
public string? Description { get; set; }
public List<UnifiedProperty> Properties { get; set; } = new();
public string? SinceVersion { get; set; }
public string? UntilVersion { get; set; }
}
internal sealed class UnifiedProperty
{
public PropertyModel Property { get; set; } = null!;
public string? SinceVersion { get; set; }
public string? UntilVersion { get; set; }
}internal sealed class UnifiedSchema
{
public List<string> Versions { get; set; } = new();
public Dictionary<string, UnifiedDefinition> Definitions { get; set; } = new();
public List<UnifiedProperty> RootProperties { get; set; } = new();
}
internal sealed class UnifiedDefinition
{
public string Name { get; set; } = "";
public string? Description { get; set; }
public List<UnifiedProperty> Properties { get; set; } = new();
public string? SinceVersion { get; set; }
public string? UntilVersion { get; set; }
}
internal sealed class UnifiedProperty
{
public PropertyModel Property { get; set; } = null!;
public string? SinceVersion { get; set; }
public string? UntilVersion { get; set; }
}This design means every property in the generated code carries its version range. Consumers can use reflection to check [SinceVersion] and [UntilVersion] attributes at runtime, or simply use IntelliSense to see version annotations in XML docs.