Ignixa.Validation
Three-tier validation system supporting Minimal, Spec, and Full validation depths.
Installation
dotnet add package Ignixa.Validation
Quick Start
using Ignixa.Validation;
using Ignixa.Validation.Abstractions;
using Ignixa.Validation.Schema;
using Ignixa.Specification.Generated;
using Ignixa.Serialization.SourceNodes;
// Get schema provider for your FHIR version
var schemaProvider = new R4CoreSchemaProvider();
// Create schema resolver
var schemaResolver = new StructureDefinitionSchemaResolver(schemaProvider);
var cachedResolver = new CachedValidationSchemaResolver(schemaResolver);
// Get validation schema - accepts type name or canonical URL
var schema = cachedResolver.GetSchema("Patient");
// Or: cachedResolver.GetSchema("http://hl7.org/fhir/StructureDefinition/Patient");
// Convert resource to IElement
var sourceNode = JsonNodeSourceNode.Create(resourceJsonNode);
var element = sourceNode.ToElement(schemaProvider);
// Validate the resource
var settings = new ValidationSettings { Depth = ValidationDepth.Spec };
// ValidationState is optional - omit for simple scenarios
var result = schema.Validate(element, settings);
// Or provide your own state for advanced scenarios (tracking, caching, etc.)
// var state = new ValidationState();
// var result = schema.Validate(element, settings, state);
if (!result.IsValid)
{
foreach (var issue in result.Issues)
{
Console.WriteLine($"{issue.Severity}: {issue.Message}");
}
}
Validation State
The ValidationState parameter is optional and can be omitted for simple scenarios. A default state will be created automatically.
When to Provide ValidationState
Provide your own ValidationState when you need:
- Shared cache across validations - Reuse expensive computations (e.g., compiled FHIRPath expressions)
- Resource tracking - Track which resources have been validated in a batch operation
- Context information - Pass resource type, ID, and location context through nested validations
// Create state with shared cache for multiple validations
var state = new ValidationState();
// Validate multiple resources with shared state
foreach (var resource in resources)
{
var element = resource.ToElement(schemaProvider);
var result = schema.Validate(element, settings, state);
// Expensive FHIRPath expressions are cached in state.Global.Cache
// state.Global.ResourcesValidated is automatically incremented
}
Console.WriteLine($"Validated {state.Global.ResourcesValidated} resources");
State Levels
ValidationState has three context levels:
| Level | Purpose | Example Use |
|---|---|---|
Global | Shared across entire validation run | Cache, resource counter |
Instance | Current resource being validated | Resource type, ID |
Location | Current element path | FHIRPath location, definition path |
// Access state information in custom checks
public class CustomCheck : IValidationCheck
{
public ValidationResult Validate(IElement element, ValidationSettings settings, ValidationState state)
{
// Access global cache
if (!state.Global.Cache.TryGetValue("myKey", out var cached))
{
cached = ExpensiveComputation();
state.Global.Cache["myKey"] = cached;
}
// Access current resource info
var resourceType = state.Instance.ResourceType;
var resourceId = state.Instance.ResourceId;
// Access current location
var path = state.Location.InstancePath;
return ValidationResult.Success();
}
}
Validation Depths
Minimal
Structural validation only - fastest option:
var settings = new ValidationSettings { Depth = ValidationDepth.Minimal };
Checks:
- JSON structure validity
- ID format validation
- Narrative structure
- Basic resource type validation
Spec
FHIR specification compliance:
var settings = new ValidationSettings { Depth = ValidationDepth.Spec };
Checks (includes Minimal, plus):
- Cardinality constraints (min/max)
- Type checking
- Reference format validation
- Choice element validation
- Required terminology bindings
- Fixed value constraints
- Pattern constraints
- Coding.system must be absolute URI
Compatibility
Microsoft FHIR Server compatibility mode for migration scenarios:
var settings = new ValidationSettings { Depth = ValidationDepth.Compatibility };
Checks (similar to Spec, but more lenient):
- All Minimal checks
- Cardinality constraints (min/max)
- Type checking
- Reference format validation
- Choice element validation
- Required terminology bindings
- Fixed value constraints
- Pattern constraints
- Accepts relative URIs in Coding.system (e.g.,
"internal-tags") - relaxed from Spec
Use when:
- Migrating from Microsoft FHIR Server (Firely SDK validation)
- Running Microsoft's FHIR Server E2E test suite
- Gradually improving data quality over time
Full
Full profile-based validation:
var settings = new ValidationSettings { Depth = ValidationDepth.Full };
Checks (includes Spec, plus):
- FHIRPath invariants
- Slice matching
- Extension validation
- Extensible terminology bindings
- Display name validation
- Advanced profile constraints
Validation Settings
var settings = new ValidationSettings
{
// Validation depth (Minimal/Spec/Full)
Depth = ValidationDepth.Full,
// Skip terminology validation if needed
SkipTerminologyValidation = false,
// How to handle terminology service failures
TerminologyFailureMode = TerminologyFailureMode.Warning,
// Optional terminology service for code validation
TerminologyService = new InMemoryTerminologyService()
};
Validation Results
Validation returns a ValidationResult:
public sealed record ValidationResult
{
public bool IsValid { get; }
public IReadOnlyList<ValidationIssue> Issues { get; }
public bool HasErrors { get; }
public bool HasWarnings { get; }
// Convert to FHIR OperationOutcome
public OperationOutcomeJsonNode ToOperationOutcome();
}
public sealed record ValidationIssue
{
public IssueSeverity Severity { get; }
public string Code { get; }
public string Path { get; }
public string Message { get; }
public CodeableConceptJsonNode? Details { get; }
}
Severity Levels
| Severity | Description | Valid Resource? |
|---|---|---|
Fatal | Cannot process | ❌ |
Error | FHIR violation | ❌ |
Warning | Best practice | ✅ |
Information | Advisory | ✅ |
Profile Validation
Against Specific Profile
// Get schema for a specific profile
var profileUrl = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient";
var schema = cachedResolver.GetSchema(profileUrl);
// Validate against the profile
var settings = new ValidationSettings { Depth = ValidationDepth.Full };
var result = schema.Validate(element, settings);
Using Custom Schema Resolvers
// Create a custom resolver that combines multiple sources
public class CustomSchemaResolver : IValidationSchemaResolver
{
private readonly IValidationSchemaResolver _baseResolver;
private readonly Dictionary<string, ValidationSchema> _customSchemas = new();
public CustomSchemaResolver(IValidationSchemaResolver baseResolver)
{
_baseResolver = baseResolver;
}
public void AddCustomSchema(string canonicalUrl, ValidationSchema schema)
{
_customSchemas[canonicalUrl] = schema;
}
public ValidationSchema? GetSchema(string canonicalUrl)
{
return _customSchemas.TryGetValue(canonicalUrl, out var schema)
? schema
: _baseResolver.GetSchema(canonicalUrl);
}
}
Custom Validation Checks
Implementing Custom Checks
public class BusinessRuleCheck : IValidationCheck
{
public ValidationResult Validate(
IElement element,
ValidationSettings settings,
ValidationState state)
{
var issues = new List<ValidationIssue>();
// Example: Require Patient to have either name or identifier
if (element.Name == "Patient")
{
var hasName = element.Children("name").Any();
var hasIdentifier = element.Children("identifier").Any();
if (!hasName && !hasIdentifier)
{
issues.Add(new ValidationIssue(
IssueSeverity.Error,
"business-rule-1",
"Patient",
"Patient must have either name or identifier"));
}
}
return issues.Any()
? ValidationResult.Failure(issues)
: ValidationResult.Success();
}
}
Adding Checks to Schema
// Build custom schema with additional checks
var baseSchema = cachedResolver.GetSchema(canonicalUrl);
var customChecks = new List<IValidationCheck>
{
new BusinessRuleCheck(),
new OrganizationPolicyCheck()
};
// Combine base checks with custom checks
var allChecks = baseSchema.Checks.Concat(customChecks).ToList();
var customSchema = new ValidationSchema(
baseSchema.CanonicalUrl,
baseSchema.ResourceType,
universalChecks: allChecks.Where(c => /* categorize */).ToList(),
specChecks: new List<IValidationCheck>(),
profileChecks: customChecks);
Terminology Validation
Using InMemoryTerminologyService
using Ignixa.Validation.Services;
// Create an in-memory terminology service
var termService = new InMemoryTerminologyService();
// Configure validation settings
var settings = new ValidationSettings
{
Depth = ValidationDepth.Full,
SkipTerminologyValidation = false,
TerminologyService = termService,
TerminologyFailureMode = TerminologyFailureMode.Warning
};
Implementing Custom Terminology Service
public class CustomTerminologyService : ITerminologyService
{
public async Task<TerminologyValidationResult> ValidateCodeAsync(
string? system,
string? code,
string? display,
string? valueSetUrl,
CancellationToken cancellationToken)
{
// Implement code validation logic
// e.g., check against external terminology server
var isValid = await CheckCodeAgainstServer(system, code, valueSetUrl);
return new TerminologyValidationResult(
IsValid: isValid,
Severity: isValid ? IssueSeverity.Information : IssueSeverity.Error,
Message: isValid ? null : $"Code {system}#{code} not found in {valueSetUrl}");
}
public async Task<BindingValidationResult> ValidateBindingAsync(
string valueSetUrl,
BindingStrength strength,
string? system,
string? code,
string? display,
string? version,
CancellationToken cancellationToken)
{
// Validate based on binding strength
// Required → Error, Extensible → Warning, Preferred → Info
var validationResult = await ValidateCodeAsync(system, code, display, valueSetUrl, cancellationToken);
return new BindingValidationResult(
IsValid: validationResult.IsValid,
Strength: strength,
Severity: DetermineSeverity(validationResult.IsValid, strength),
Message: validationResult.Message,
SuggestedDisplay: null);
}
// Implement other ITerminologyService methods (LookupCodeAsync, ExpandValueSetAsync, etc.)...
}
Batch Validation
Multiple Resources
// Validate multiple resources in parallel
var schemaProvider = new R4CoreSchemaProvider();
var schemaResolver = new CachedValidationSchemaResolver(
new StructureDefinitionSchemaResolver(schemaProvider));
var results = new ConcurrentBag<ValidationResult>();
var settings = new ValidationSettings { Depth = ValidationDepth.Spec };
await Parallel.ForEachAsync(resources, async (resourceNode, ct) =>
{
var sourceNode = JsonNodeSourceNode.Create(resourceNode.MutableNode);
var element = sourceNode.ToElement(schemaProvider);
var resourceType = resourceNode.ResourceType;
var schema = schemaResolver.GetSchema(
$"http://hl7.org/fhir/StructureDefinition/{resourceType}");
if (schema != null)
{
var result = schema.Validate(element, settings);
results.Add(result);
}
});
Bundle Validation
// Validate entire bundle as a resource
var bundleSchema = schemaResolver.GetSchema(
"http://hl7.org/fhir/StructureDefinition/Bundle");
var bundleElement = JsonNodeSourceNode.Create(bundle.MutableNode)
.ToElement(schemaProvider);
var bundleResult = bundleSchema.Validate(bundleElement, settings);
// Validate each entry resource individually
if (bundle.Entry != null)
{
foreach (var entry in bundle.Entry)
{
if (entry.Resource != null)
{
var entryElement = JsonNodeSourceNode.Create(entry.Resource.MutableNode)
.ToElement(schemaProvider);
var entrySchema = schemaResolver.GetSchema(
$"http://hl7.org/fhir/StructureDefinition/{entry.Resource.ResourceType}");
var entryResult = entrySchema?.Validate(entryElement, settings);
}
}
}
CLI Tool
The ignixa-validator tool validates FHIR resources from the command line.
Installation
dotnet tool install --global Ignixa.Validation.Cli
Usage
# Validate a file
ignixa-validator r4 --input patient.json --console
# Validate inline JSON
ignixa-validator r4 --json '{"resourceType":"Patient","id":"123"}' --console
# Output OperationOutcome to file
ignixa-validator r4 --input patient.json --out result.json
# Use different FHIR versions
ignixa-validator r5 --input patient.json --console
ignixa-validator stu3 --input patient.json --console
Options
| Option | Description |
|---|---|
--input <file> | Path to JSON file to validate |
--json <string> | Inline JSON string to validate |
--out <file> | Output file for OperationOutcome JSON |
--console | Display formatted results in console |
Migration from Microsoft FHIR Server
The Compatibility validation depth is specifically designed for migration scenarios from Microsoft FHIR Server (which uses Firely SDK validation).
Validation Depth Comparison
| Validation Depth | Coding.system Absolute URI | Terminology Bindings | FHIRPath Invariants | Use Case |
|---|---|---|---|---|
Minimal | Not checked | Not checked | Not checked | Bulk ingestion, high throughput |
Compatibility | Not enforced (accepts relative URIs) | Required only | Not checked | Microsoft FHIR Server migration |
Spec | Enforced (absolute URI required) | Required only | Not checked | Standard API operations |
Full | Enforced (absolute URI required) | All bindings | Checked | Compliance testing, IG validation |
Key Differences: Compatibility vs Spec
The Compatibility depth is more lenient than Spec in these areas:
Coding.system URIs:
Compatibility: Accepts relative or local references (e.g.,"internal-tags","local-system")Spec: Requires absolute URIs (e.g.,"http://terminology.hl7.org/CodeSystem/v3-ActCode")
This is common in meta.tag fields used for internal categorization.
Example: Compatibility Mode
var settings = new ValidationSettings
{
Depth = ValidationDepth.Compatibility
};
var result = schema.Validate(element, settings);
// Accepts resources with relative URIs in Coding.system
Resource that passes at Compatibility but fails at Spec:
{
"resourceType": "Medication",
"meta": {
"tag": [{
"system": "internal-tags",
"code": "test-medication"
}]
},
"code": {
"coding": [{
"system": "http://www.nlm.nih.gov/research/umls/rxnorm",
"code": "1234",
"display": "Test Med"
}]
}
}
- ✅ Accepted at
Compatibilitydepth - ❌ Rejected at
SpecandFulldepths (due to"system": "internal-tags")
Migration Path
Follow this path to gradually improve data quality:
-
Start with Compatibility: Validate existing data with
ValidationDepth.Compatibilityvar settings = new ValidationSettings { Depth = ValidationDepth.Compatibility }; -
Identify Issues: Review resources that would fail at
Specdepth// Test against Spec to find issues
var specSettings = new ValidationSettings { Depth = ValidationDepth.Spec };
var specResult = schema.Validate(element, specSettings);
// Log issues for data cleanup -
Fix Data Quality: Gradually update relative URIs to absolute URIs
- Replace
"internal-tags"with"http://your-organization.com/fhir/CodeSystem/internal-tags" - Document your internal code systems with CodeSystem resources
- Replace
-
Upgrade to Spec: Once data is compliant, switch to
ValidationDepth.Specvar settings = new ValidationSettings { Depth = ValidationDepth.Spec }; -
Full Validation: Eventually enable
ValidationDepth.Fullfor complete FHIR compliancevar settings = new ValidationSettings { Depth = ValidationDepth.Full };
Usage Guidelines
| Depth | Use Case |
|---|---|
| Minimal | Bulk ingestion, high throughput scenarios |
| Compatibility | Microsoft FHIR Server migration, E2E test compatibility |
| Spec | Standard API operations, general-purpose validation |
| Full | Compliance testing, IG validation, profile conformance |
Optimization Tips
- Use CachedValidationSchemaResolver - Caches compiled schemas to avoid rebuilding checks
- Choose appropriate depth - Use Minimal for bulk ingestion, Full only when needed
- Reuse ValidationSettings - Create once and reuse across validations
- Parallel validation - Validate multiple resources concurrently (schemas are thread-safe)
- Skip terminology when possible - Set
SkipTerminologyValidation = truefor performance