instructure/canvas-lms · error · ImportError
Invalid type '# ' for change_sis_id
Error message
Invalid type '#{type}' for change_sis_id What it means
After downcasing/stripping the type, process_change_sis_id looks it up in a fixed hash of seven supported types (user, course, section, term, account, group, group_category). Any other value raises this ImportError because there is no scope/column mapping for it.
Solutions
- Use exactly one of: user, course, section, term, account, group, group_category (case/whitespace is normalized).
- To change a user's SIS login id note 'user' maps to Pseudonyms with column sis_user_id.
- Reject unsupported types at CSV-generation time in the upstream system.
Example fix
// before type,old_id,new_id enrollment,E1,E2 // after type,old_id,new_id course,C1,C2
Defensive patterns
Strategy: validation
Validate before calling
VALID_TYPES = %w[user course section term account group group_category]
unless VALID_TYPES.include?(dc.type.to_s.downcase.strip)
raise ArgumentError, "unsupported type: #{dc.type}"
end
process_change_sis_id(dc) Type guard
VALID_TYPES = %w[user course section term account group group_category].freeze def supported_type?(dc) VALID_TYPES.include?(dc.type.to_s.downcase.strip) end
Try / catch
begin
process_change_sis_id(dc)
rescue SIS::ImportError => e
errors << e.message if e.message.start_with?('Invalid type')
end Prevention
- Keep a shared constant of the seven valid types in the generating system.
- Only enrollments/assignments etc. are not re-keyable via change_sis_id — use the regular SIS CSV flow for those.
- Normalize case/whitespace on both sides before comparing types.
When it happens
Trigger: type values like 'enrollment', 'assignment', 'User ' variants that map to nothing after downcase/strip (e.g. 'pseudonym', 'users', 'group_category ' misspelled as 'groupcategory'), or singular/plural mismatches.
Common situations: Assuming any Canvas object type is changeable; typos in hand-written CSVs; integrations emitting their own internal type names instead of Canvas's change_sis_id type vocabulary.
Understand the failure class
Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.
Related errors
- A new_id, '# ', referenced an existing # and the # with #…
- A new_integration_id, '#
- A student referenced a non-existent user #
- An old_id, '# ', referenced a different # than the…
- An old_id, '# ', referenced a non-existent # and was not…
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/9e772aef753db845.
Report an issue: GitHub.
Appendix: source
Thrown at lib/sis/change_sis_id_importer.rb:86
raise ImportError, "No type given for change_sis_id" if data_change.type.blank?
raise ImportError, "No old_id or old_integration_id given for change_sis_id" if data_change.old_id.blank? && data_change.old_integration_id.blank?
raise ImportError, "No new_id or new_integration_id given for change_sis_id" if data_change.new_id.blank? && data_change.new_integration_id.blank?
type = data_change.type.downcase.strip
things_to_update_batch_ids[type] ||= Set.new
types = {
"user" => { scope: @root_account.pseudonyms, column: :sis_user_id },
"course" => { scope: @root_account.all_courses },
"section" => { scope: @root_account.course_sections },
"term" => { scope: @root_account.enrollment_terms },
"account" => { scope: @root_account.all_accounts },
"group" => { scope: @root_account.all_groups },
"group_category" => { scope: @root_account.all_group_categories },
}
details = types[type]
raise ImportError, "Invalid type '#{type}' for change_sis_id" unless details
column = details[:column] || :sis_source_id
check_for_conflicting_ids(column, details, type, data_change)
old_item = find_item_to_update(column, details, type, data_change)
update_record(column, data_change, details, old_item)
things_to_update_batch_ids[type] << old_item.id
users_to_sync << old_item.user_id if type == "user"
self.success_count += 1
end
def update_record(column, data_change, details, old_item)
updates = ids_to_change(column, data_change)
details[:scope].where(id: old_item.id).update_all(updates)
old_item.invalidate_association_cache if old_item.is_a?(Account)
# update_all bypasses AR callbacks; re-index GlobalLookups
# so cross-shard searches can find the new IDs.
if old_item.is_a?(Pseudonym) && GlobalLookups.enabled?
old_item.delay_if_production.ensure_global_lookup_record(force: true)View on GitHub (pinned to 1c9f0bb801)