{"record":{"id":"a0587099f36493ad","repo":"apache/pulsar","slug":"output-schema-mismatch","errorCode":null,"errorMessage":"Output Schema mismatch","messagePattern":"Output Schema mismatch","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"pulsar-functions/utils/src/main/java/org/apache/pulsar/functions/utils/FunctionConfigUtils.java","lineNumber":1060,"sourceCode":"        if (!newConfig.getInputSpecs().isEmpty()) {\n            newConfig.getInputSpecs().forEach((topicName, consumerConfig) -> {\n                if (!existingConfig.getInputSpecs().containsKey(topicName)) {\n                    throw new IllegalArgumentException(\"Input Topics cannot be altered\");\n                }\n                if (consumerConfig.isRegexPattern() != existingConfig.getInputSpecs().get(topicName).isRegexPattern()) {\n                    throw new IllegalArgumentException(\n                            \"isRegexPattern for input topic \" + topicName + \" cannot be altered\");\n                }\n                mergedConfig.getInputSpecs().put(topicName, consumerConfig);\n            });\n        }\n        if (!StringUtils.isEmpty(newConfig.getOutputSerdeClassName()) && !newConfig.getOutputSerdeClassName()\n                .equals(existingConfig.getOutputSerdeClassName())) {\n            throw new IllegalArgumentException(\"Output Serde mismatch\");\n        }\n        if (!StringUtils.isEmpty(newConfig.getOutputSchemaType()) && !newConfig.getOutputSchemaType()\n                .equals(existingConfig.getOutputSchemaType())) {\n            throw new IllegalArgumentException(\"Output Schema mismatch\");\n        }\n        if (!StringUtils.isEmpty(newConfig.getLogTopic())) {\n            mergedConfig.setLogTopic(newConfig.getLogTopic());\n        }\n        if (newConfig.getProcessingGuarantees() != null && !newConfig.getProcessingGuarantees()\n                .equals(existingConfig.getProcessingGuarantees())) {\n            throw new IllegalArgumentException(\"Processing Guarantees cannot be altered\");\n        }\n        if (newConfig.getRetainOrdering() != null && !newConfig.getRetainOrdering()\n                .equals(existingConfig.getRetainOrdering())) {\n            throw new IllegalArgumentException(\"Retain Ordering cannot be altered\");\n        }\n        if (newConfig.getRetainKeyOrdering() != null && !newConfig.getRetainKeyOrdering()\n                .equals(existingConfig.getRetainKeyOrdering())) {\n            throw new IllegalArgumentException(\"Retain Key Ordering cannot be altered\");\n        }\n        if (!StringUtils.isEmpty(newConfig.getOutput())) {\n            mergedConfig.setOutput(newConfig.getOutput());","sourceCodeStart":1042,"sourceCodeEnd":1078,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-functions/utils/src/main/java/org/apache/pulsar/functions/utils/FunctionConfigUtils.java#L1042-L1078","documentation":"The declared output schema type of a function is immutable on update. When the new config provides a non-empty outputSchemaType that differs from the existing config's, validateUpdate throws this error because consumers depend on the published output schema.","triggerScenarios":"Updating a function where newConfig.getOutputSchemaType() is non-empty and not equal to existingConfig.getOutputSchemaType() (e.g. AVRO -> JSON or a schema class name change).","commonSituations":"Evolving a function's output schema in place; migrating from serdeClassName-based config to schemaType-based config where one field is set inconsistently; copy-pasted configs from other functions.","solutions":["Pass the existing outputSchemaType unchanged, or omit it (empty) in the update.","Delete and recreate the function if the output schema must change.","Ensure any schema-migration path uses a new function (or a compatibility-checked recreate) rather than an in-place update."],"exampleFix":"// before\nnewConfig.setOutputSchemaType(\"JSON\"); // existing: \"AVRO\"\n// after\nnewConfig.setOutputSchemaType(\"\"); // keep existing schema on update\n// or recreate the function with the new schema type","handlingStrategy":"validation","validationCode":"if (newConfig.getOutputSchemaType() != null && !newConfig.getOutputSchemaType().isEmpty()\n        && !newConfig.getOutputSchemaType().equals(existingConfig.getOutputSchemaType())) {\n    throw new IllegalArgumentException(\"outputSchemaType is immutable on update\");\n}","typeGuard":"boolean outputSchemaUnchanged(FunctionConfig existing, FunctionConfig updated) {\n    String n = updated.getOutputSchemaType();\n    return n == null || n.isEmpty() || n.equals(existing.getOutputSchemaType());\n}","tryCatchPattern":"try {\n    merged = FunctionConfigUtils.validateUpdate(existing, updated);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().equals(\"Output Schema mismatch\")) {\n        updated.setOutputSchemaType(\"\");\n        merged = FunctionConfigUtils.validateUpdate(existing, updated);\n    } else { throw e; }\n}","preventionTips":["Never change outputSchemaType in an update; plan schema evolution as recreate.","Keep schemaType and serdeClassName config styles consistent across environments.","Diff output schema fields in the deploy pipeline before updating."],"tags":["pulsar-functions","immutable-field","update-validation","schema"],"backgroundTag":"immutable-field-update-rejected","analyzedSha":"820761864ed8e2a7d2e52dd9763ad2ae117c1395","analyzedAt":"2026-09-06T00:14:20.138Z","contentChangedAt":"2026-09-06T00:14:20.138Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}