validateJsonPayload static method
Validate raw JSON payload before/after parsing.
When deep is true, validator also parses JSON into BaseChartConfig
and runs type-specific config validation.
Implementation
static ValidationResult validateJsonPayload(
Map<String, dynamic> json, {
bool deep = true,
bool requireRegisteredType = false,
}) {
final issues = <ValidationIssue>[];
final rawType = (json['type'] ?? '').toString().trim();
final parsedType = rawType.isEmpty ? ChartType.line : getChartType(rawType);
if (rawType.isEmpty) {
issues.add(
const ValidationIssue(
severity: ValidationSeverity.error,
code: 'MISSING_TYPE',
message: 'Missing required field: "type".',
field: 'type',
suggestion: 'Provide chart type, e.g. "bar", "line", "pie".',
),
);
} else if (_isLikelyUnknownType(rawType, parsedType)) {
issues.add(
ValidationIssue(
severity: ValidationSeverity.error,
code: 'UNKNOWN_TYPE',
message: 'Unknown chart type "$rawType".',
field: 'type',
suggestion: _typeSuggestion(
rawType,
fallback: 'Use a supported chart type or register a custom type.',
),
),
);
}
if (requireRegisteredType &&
rawType.isNotEmpty &&
!ChartRegistry.isRegisteredString(rawType)) {
issues.add(
ValidationIssue(
severity: ValidationSeverity.error,
code: 'UNREGISTERED_TYPE',
message: 'Chart type "$rawType" is not registered in ChartRegistry.',
field: 'type',
suggestion: _typeSuggestion(
rawType,
fallback: 'Register the type (or relevant bundle) before parsing.',
),
),
);
}
final rawMode = json['dataMode'] ?? json['datasetMode'];
final normalizedMode = rawMode is String
? rawMode.trim().toLowerCase()
: null;
if (rawMode != null) {
if (rawMode is! String) {
issues.add(
const ValidationIssue(
severity: ValidationSeverity.error,
code: 'INVALID_DATA_MODE_TYPE',
message: '"dataMode" must be a string.',
field: 'dataMode',
suggestion: 'Use one of: regular, auto, large.',
),
);
} else {
const allowed = {
'regular',
'simple',
'auto',
'large',
'largedataset',
'performance',
};
if (!allowed.contains(normalizedMode)) {
issues.add(
ValidationIssue(
severity: ValidationSeverity.error,
code: 'INVALID_DATA_MODE_VALUE',
message: 'Unsupported dataMode "$rawMode".',
field: 'dataMode',
suggestion: 'Use one of: regular, auto, large.',
),
);
}
}
}
final rawSampling = json['sampling'];
if (rawSampling != null) {
if (rawSampling is! Map) {
issues.add(
const ValidationIssue(
severity: ValidationSeverity.error,
code: 'INVALID_SAMPLING_TYPE',
message: '"sampling" must be an object.',
field: 'sampling',
),
);
} else {
final enabled = rawSampling['enabled'];
if (enabled != null && enabled is! bool) {
issues.add(
const ValidationIssue(
severity: ValidationSeverity.error,
code: 'INVALID_SAMPLING_ENABLED_TYPE',
message: '"sampling.enabled" must be a boolean.',
field: 'sampling.enabled',
),
);
}
final threshold = rawSampling['threshold'];
if (threshold != null) {
if (threshold is! num) {
issues.add(
const ValidationIssue(
severity: ValidationSeverity.error,
code: 'INVALID_SAMPLING_THRESHOLD_TYPE',
message: '"sampling.threshold" must be a number.',
field: 'sampling.threshold',
),
);
} else {
final t = _toPositiveInt(threshold);
if (t == null) {
issues.add(
ValidationIssue(
severity: ValidationSeverity.error,
code: 'INVALID_SAMPLING_THRESHOLD_VALUE',
message:
'"sampling.threshold" must be a finite number greater than 0, got $threshold.',
field: 'sampling.threshold',
suggestion: 'Use 200-1500 for most cartesian charts.',
),
);
} else if (t < 10) {
issues.add(
ValidationIssue(
severity: ValidationSeverity.warning,
code: 'LOW_SAMPLING_THRESHOLD',
message:
'"sampling.threshold" is very low ($t) and may distort the chart.',
field: 'sampling.threshold',
),
);
}
}
}
final strategy = rawSampling['strategy'];
if (strategy != null) {
if (strategy is! String) {
issues.add(
const ValidationIssue(
severity: ValidationSeverity.error,
code: 'INVALID_SAMPLING_STRATEGY_TYPE',
message: '"sampling.strategy" must be a string.',
field: 'sampling.strategy',
),
);
} else {
final normalized = strategy.trim().toLowerCase();
const allowed = {
'auto',
'lttb',
'minmax',
'min_max',
'nth',
'every_n',
};
if (!allowed.contains(normalized)) {
issues.add(
ValidationIssue(
severity: ValidationSeverity.error,
code: 'INVALID_SAMPLING_STRATEGY_VALUE',
message: 'Unsupported sampling strategy "$strategy".',
field: 'sampling.strategy',
suggestion: 'Use auto, lttb, minMax, or nth.',
),
);
}
}
}
if (normalizedMode == 'regular') {
if (enabled == true) {
issues.add(
const ValidationIssue(
severity: ValidationSeverity.info,
code: 'REGULAR_MODE_SAMPLING_IGNORED',
message:
'dataMode "regular" ignores sampling settings for rendering.',
field: 'sampling',
),
);
}
}
}
}
final samplingRequestedByMode =
normalizedMode == 'large' ||
normalizedMode == 'largedataset' ||
normalizedMode == 'performance';
final samplingRequestedByConfig =
rawSampling != null &&
(rawSampling is! Map ||
rawSampling['enabled'] == true ||
rawSampling.containsKey('threshold') ||
rawSampling.containsKey('strategy'));
final hasTypeError = issues.any(
(i) => i.code == 'MISSING_TYPE' || i.code == 'UNKNOWN_TYPE',
);
if (!hasTypeError &&
(samplingRequestedByMode || samplingRequestedByConfig) &&
!_typeSupportsLargeDataSampling(parsedType)) {
issues.add(
ValidationIssue(
severity: ValidationSeverity.warning,
code: 'SAMPLING_LIKELY_IGNORED_BY_TYPE',
message:
'Sampling/dataMode may be ignored for chart type "${chartTypeToString(parsedType)}".',
field: 'sampling',
suggestion:
'Use this mainly on cartesian/trend/trading chart types, or keep dataMode as "regular".',
),
);
}
_validateRuntimePerformancePolicyPayload(issues, json);
_validateDiagnosticFallbackOptionsPayload(issues, json);
_validatePayloadNormalizationOptionsPayload(issues, json);
final structuralJson = normalizeDataCollectionPayload(json);
final requiresSeries = _typeRequiresSeries(parsedType);
final seriesRaw = structuralJson['series'];
if (requiresSeries && seriesRaw == null) {
issues.add(
const ValidationIssue(
severity: ValidationSeverity.error,
code: 'MISSING_SERIES',
message: 'Missing required field: "series".',
field: 'series',
suggestion: 'Provide a non-empty series list.',
),
);
} else if (seriesRaw != null && seriesRaw is! List) {
issues.add(
const ValidationIssue(
severity: ValidationSeverity.error,
code: 'INVALID_SERIES_TYPE',
message: '"series" must be a List.',
field: 'series',
),
);
} else if (seriesRaw is List) {
if (requiresSeries && seriesRaw.isEmpty) {
issues.add(
const ValidationIssue(
severity: ValidationSeverity.warning,
code: 'EMPTY_SERIES',
message: '"series" is empty.',
field: 'series',
suggestion: 'Add at least one series with data.',
),
);
}
for (int i = 0; i < seriesRaw.length; i++) {
final item = seriesRaw[i];
if (item is! Map) {
issues.add(
ValidationIssue(
severity: ValidationSeverity.error,
code: 'INVALID_SERIES_ITEM',
message: 'series[$i] must be a JSON object.',
field: 'series[$i]',
),
);
continue;
}
if (item['data'] != null && item['data'] is! List) {
issues.add(
ValidationIssue(
severity: ValidationSeverity.error,
code: 'INVALID_DATA_TYPE',
message: 'series[$i].data must be a List.',
field: 'series[$i].data',
),
);
}
}
}
final hasPayloadShapeError = issues.any(
(issue) =>
issue.code == 'MISSING_TYPE' ||
issue.code == 'UNKNOWN_TYPE' ||
issue.code == 'MISSING_SERIES' ||
issue.code == 'INVALID_SERIES_TYPE' ||
issue.code == 'INVALID_SERIES_ITEM' ||
issue.code == 'INVALID_DATA_TYPE',
);
if (!hasPayloadShapeError) {
_validateSeriesShapeCompatibility(issues, structuralJson, parsedType);
}
_validateBarRacePayload(issues, parsedType, json);
_validateFinancialSeriesPayload(issues, parsedType, seriesRaw);
_validateTradingSeriesPayload(
issues,
parsedType,
structuralJson,
seriesRaw,
);
if (deep && issues.where((e) => e.isError).isEmpty) {
try {
final config = BaseChartConfig.fromJson(structuralJson);
final deepResult = validate(config);
issues.addAll(deepResult.issues);
} catch (e) {
issues.add(
ValidationIssue(
severity: ValidationSeverity.error,
code: 'PAYLOAD_PARSE_FAILED',
message: 'Payload parsing failed: $e',
suggestion:
'Check JSON structure for this chart type and field names.',
),
);
}
}
return ValidationResult(issues: issues, type: parsedType);
}