validateJsonPayload static method

ValidationResult validateJsonPayload(
  1. Map<String, dynamic> json, {
  2. bool deep = true,
  3. bool requireRegisteredType = false,
})

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);
}