{"record":{"id":"908df4dad35a8945","repo":"opf/openproject","slug":"filter-must-be-a-json-object-got-filter-class","errorCode":null,"errorMessage":"Filter must be a JSON object, got #{filter.class}","messagePattern":"Filter must be a JSON object, got #(.+?)","errorType":"http","errorClass":"JSON::ParserError","httpStatus":400,"severity":"error","filePath":"app/services/api/v3/parse_query_params_service.rb","lineNumber":139,"sourceCode":"      #       \"values\": [\"values\", \"for the\", \"operation\"]\n      #     }\n      #   },\n      #   { /* more filters if needed */}\n      # ]\n      def filters_from_params(params)\n        filters = params[:filters] || params[:filter]\n        return unless filters\n\n        filters = JSON.parse filters if filters.is_a? String\n\n        filters.map do |filter|\n          filter_from_params(filter)\n        end\n      end\n\n      def filter_from_params(filter)\n        unless filter.is_a?(Hash)\n          raise JSON::ParserError, \"Filter must be a JSON object, got #{filter.class}\"\n        end\n\n        attribute = filter.keys.first # there should only be one attribute per filter\n        operator =  filter[attribute][\"operator\"]\n        values = Array(filter[attribute][\"values\"])\n        ar_attribute = convert_filter_attribute attribute, append_id: true\n\n        { field: ar_attribute,\n          operator:,\n          values: }\n      end\n\n      def columns_from_params(params)\n        columns = params_value(params, KEYS_COLUMNS)\n\n        return unless columns\n\n        columns.map do |column|","sourceCodeStart":121,"sourceCodeEnd":157,"githubUrl":"https://github.com/opf/openproject/blob/d9742c43f3424c34b63550f8c03f201fe5c3040c/app/services/api/v3/parse_query_params_service.rb#L121-L157","documentation":"API::V3::ParseQueryParamsService parses the filters parameter of API v3 work package queries. After JSON.parsing the string, each element must be a Hash with exactly one attribute key mapping to an object with 'operator' and 'values'; any other shape raises JSON::ParserError with this class-revealing message. The service rescues it in json_parsed_params and returns a failed ServiceResult, so API clients see it as an error payload on the request.","triggerScenarios":"Calling an endpoint that uses this service (e.g. GET /api/v3/work_packages or POST /api/v3/work_packages/form) with filters as a single JSON object ('{\"status\":{...}}' — Hash#map then yields Array pairs), as nested arrays ('[[\"status\",\"=\",\"1\"]]'), or as an array of strings ('[\"status\"]'). A JSON string that fails JSON.parse at line 130 raises the standard JSON::ParserError instead.","commonSituations":"Migrating clients from old nested-array filter syntax to the v3 object form; hand-built query strings that forget the outer array around a single filter; forgetting to URL-encode brackets; copy-pasting a single filter object from API docs instead of the array wrapper.","solutions":["Send filters as an array of single-key objects: [{\"status\":{\"operator\":\"=\",\"values\":[\"1\"]}}] (URL-encoded)","Wrap a single filter in an array — a bare object is the most common mistake","Validate the payload shape client-side before sending: every element is an object with one key whose value has 'operator' and 'values'","Read the class name in the error message ('got Array' / 'got String') to see which shape actually arrived, then compare with the documented example above the filters_from_params method"],"exampleFix":"# before — both raise \"Filter must be a JSON object, got ...\"\nGET /api/v3/work_packages?filters={\"status\":{\"operator\":\"=\",\"values\":[\"1\"]}}\nGET /api/v3/work_packages?filters=[[\"status\",\"=\",\"1\"]]\n\n# after — array of single-key objects, URL-encoded\nGET /api/v3/work_packages?filters=%5B%7B%22status%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D","handlingStrategy":"validation","validationCode":"# build and check the filters payload before the request\nfilters = [{ \"status\" => { \"operator\" => \"=\", \"values\" => [\"1\"] } }]\nunless valid_api_v3_filters?(filters)\n  raise ArgumentError, \"filters must be an array of single-key objects with operator/values\"\nend\nJSON.generate(filters)","typeGuard":"# Ruby: validates the parsed shape the service expects\nvalid_api_v3_filters?(parsed)\n  parsed.is_a?(Array) && parsed.all? do |f|\n    f.is_a?(Hash) && f.size == 1 && f.values.first.is_a?(Hash) &&\n      f.values.first.key?(\"operator\") && f.values.first.key?(\"values\")\n  end\nend","tryCatchPattern":null,"preventionTips":["Generate filters programmatically (JSON.generate of Ruby hashes) instead of string concatenation — this prevents both shape and encoding mistakes","Keep one canonical filter builder shared by all API clients","Remember the exact contract: array of objects, one key each, value has operator + values"],"tags":["api","json","filters","query","validation","rest","openproject-api"],"backgroundTag":"api-query-parameter-invalid","analyzedSha":"d9742c43f3424c34b63550f8c03f201fe5c3040c","analyzedAt":"2026-08-21T14:40:06.829Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}