{"record":{"id":"684ce16449656252","repo":"docusealco/docuseal","slug":"templates-modifydocuments-invalidlayout","errorCode":null,"errorMessage":"Templates::ModifyDocuments::InvalidLayout","messagePattern":"Templates::ModifyDocuments::InvalidLayout","errorType":"validation","errorClass":"Templates::ModifyDocuments::InvalidLayout","httpStatus":422,"severity":"error","filePath":"lib/templates/modify_documents.rb","lineNumber":93,"sourceCode":"\n            schema_item.except('google_drive_file_id').merge('attachment_uuid' => document.uuid)\n          end\n        end\n      ensure\n        sources.each_value(&:close)\n      end\n    end\n\n    def add_page_mapping(mapping, ref, target)\n      mapping[[ref['attachment_uuid'], ref['page']]] = target\n\n      replaced = ref['replaced_page']\n\n      mapping[[replaced['attachment_uuid'], replaced['page']]] = target if replaced\n    end\n\n    def validate_layout!(template, documents_layout, attachments_index)\n      raise InvalidLayout if documents_layout.blank?\n      raise InvalidLayout if documents_layout.all? { |entry| entry['pages'].blank? }\n\n      dynamic_uuids = template.schema.select { |item| item['dynamic'] }.pluck('attachment_uuid')\n      non_dynamic_uuids = template.schema.pluck('attachment_uuid') - dynamic_uuids\n      layout_uuids = documents_layout.pluck('attachment_uuid')\n\n      raise InvalidLayout if layout_uuids.uniq.size != layout_uuids.size\n      raise InvalidLayout if (non_dynamic_uuids - layout_uuids).any?\n      raise InvalidLayout if layout_uuids.intersect?(dynamic_uuids)\n      raise InvalidLayout if layout_uuids.any? { |uuid| attachments_index[uuid].nil? }\n\n      refs = documents_layout.flat_map { |entry| entry['pages'].to_a }\n\n      refs.each { |ref| validate_ref!(ref, attachments_index) }\n\n      ref_keys = refs.map { |ref| [ref['attachment_uuid'], ref['page']] }\n\n      raise InvalidLayout if ref_keys.uniq.size != ref_keys.size","sourceCodeStart":75,"sourceCodeEnd":111,"githubUrl":"https://github.com/docusealco/docuseal/blob/004a22c1c88109c7ba0b567df011a8cb13894001/lib/templates/modify_documents.rb#L75-L111","documentation":"Raised by Templates::ModifyDocuments.validate_layout! when the documents_layout argument passed to Templates::ModifyDocuments.call(template, documents_layout) is blank (nil or an empty array). The layout is the authoritative description of the template's new document structure, so 'nothing' is treated as a malformed request rather than 'no change'.","triggerScenarios":"Calling Templates::ModifyDocuments.call(template, nil) or (template, []) — e.g. an API client omits documents_layout from the request body, or upstream code passes params[:documents_layout].presence, which collapses an empty array to nil.","commonSituations":"A document-editor 'apply changes' request sent with an empty body; serialization that drops empty arrays; a guard elsewhere using .presence turning [] into nil; automated jobs running the modifier for every template including ones with no pending edits.","solutions":["Skip the ModifyDocuments call entirely when the layout is blank — blank means there is nothing to apply","Guard at the call site: return template unless documents_layout.is_a?(Array) && documents_layout.present?","Fix the client to always send the full layout array whenever it requests a document modification"],"exampleFix":"# before\nTemplates::ModifyDocuments.call(template, params[:documents_layout].presence) # nil -> InvalidLayout\n# after\nlayout = params[:documents_layout]\nTemplates::ModifyDocuments.call(template, layout) if layout.is_a?(Array) && layout.present?","handlingStrategy":"validation","validationCode":"layout.is_a?(Array) && layout.present?","typeGuard":"layout.is_a?(Array) && !layout.empty?","tryCatchPattern":"begin\n  Templates::ModifyDocuments.call(template, documents_layout)\nrescue Templates::ModifyDocuments::InvalidLayout => e\n  render json: { error: 'Invalid document layout' }, status: :unprocessable_entity\nend","preventionTips":["Treat an absent layout as 'no-op' and short-circuit before calling the modifier","Never use .presence when forwarding layout params — it silently converts [] to nil","Add a JSON-schema check on documents_layout at the API boundary (non-empty array of objects)"],"tags":["ruby","rails","e-signature","document-layout","pdf"],"backgroundTag":"document-layout-validation-failed","analyzedSha":"004a22c1c88109c7ba0b567df011a8cb13894001","analyzedAt":"2026-08-21T13:38:23.343Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}