{"record":{"id":"1505cdeefa314d7c","repo":"instructure/canvas-lms","slug":"filters-must-be-an-object","errorCode":null,"errorMessage":"filters must be an object","messagePattern":"filters must be an object","errorType":"validation","errorClass":"GraphQL::ExecutionError","httpStatus":null,"severity":"error","filePath":"app/graphql/mutations/update_widget_dashboard_config.rb","lineNumber":74,"sourceCode":"  VALID_ANNOUNCEMENT_FILTERS = %w[unread read all].freeze\n  VALID_TODO_FILTERS = %w[incomplete_items complete_items all].freeze\n  VALID_DATE_FILTERS = %w[not_submitted missing submitted].freeze\n\n  private\n\n  # TODO: Remove this entire helper method once platform-ui passes dashboard_type (EGG-2539)\n  # The resolver should directly use input[:dashboard_type] after that.\n  def educator_dashboard?(input)\n    return input[:dashboard_type] == \"educator\" if input[:dashboard_type].present?\n\n    context[:domain_root_account]&.feature_enabled?(:educator_dashboard) &&\n      current_user.educator_dashboard_user?\n  end\n\n  def validate_filters!(widget_id, filters)\n    # Accept Hash or ActionController::Parameters (which GraphQL JSON type may produce)\n    unless filters.is_a?(Hash) || filters.is_a?(ActionController::Parameters)\n      raise GraphQL::ExecutionError, \"filters must be an object\"\n    end\n\n    filters.each_key do |key|\n      unless key.is_a?(String) && !key.empty?\n        raise GraphQL::ExecutionError, \"filter keys must be non-empty strings\"\n      end\n    end\n\n    validate_filter_structure!(widget_id, filters)\n  end\n\n  def validate_filter_structure!(widget_id, filters)\n    if widget_id == ANNOUNCEMENTS_WIDGET_ID\n      validate_announcements_filters!(filters)\n    elsif widget_id == TODO_LIST_WIDGET_ID\n      validate_todo_list_filters!(filters)\n    elsif COURSE_WORK_WIDGET_IDS.include?(widget_id)\n      validate_course_work_filters!(filters)","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/graphql/mutations/update_widget_dashboard_config.rb#L56-L92","documentation":"UpdateWidgetDashboardConfig's validate_filters! requires the filters argument (a GraphQL JSON value) to be a Hash or ActionController::Parameters. Any other JSON type — array, string, number, null — raises this error before per-key validation.","triggerScenarios":"Calling updateWidgetDashboardConfig with filters set to a JSON array, scalar, or null instead of an object like {\"courseIds\": [1,2]}.","commonSituations":"Client sends filters: [] (an empty array) as a 'clear' shortcut; JSON serialization turns an object into a scalar; API consumer misreads schema and sends a list of filters.","solutions":["Send filters as a JSON object whose keys are filter names","Replace filters: [] with filters: {} to clear filters","Coerce/validate the payload client-side before the mutation","Ensure your HTTP layer preserves object shape (no accidental flattening)"],"exampleFix":"// before\nupdateWidgetDashboardConfig(input: {widgetId, filters: []})\n// after\nupdateWidgetDashboardConfig(input: {widgetId, filters: {}}) // clear","handlingStrategy":"type-guard","validationCode":"const isFiltersObject = (f) => f !== null && typeof f === 'object' && !Array.isArray(f)","typeGuard":"const asFilters = (f) => (f !== null && typeof f === 'object' && !Array.isArray(f) ? f : {})","tryCatchPattern":"try { await gql(updateWidgetDashboardConfigMutation, {widgetId, filters}) } catch (e) { if (/filters must be an object/.test(e.message)) filters = {}; else throw e }","preventionTips":["Never send arrays or scalars as filters","Use {} to clear filters","Validate payload shape with a schema (zod/JSON Schema) before sending"],"tags":["graphql","validation","type-error"],"backgroundTag":"invalid-argument-value","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}