Skip to content

refactor: 응답의 optional 필드 표기를 nullable로 통일 - #147

Open
1117mg wants to merge 1 commit into
devfrom
refactor/api-nullable-field-convention
Open

refactor: 응답의 optional 필드 표기를 nullable로 통일#147
1117mg wants to merge 1 commit into
devfrom
refactor/api-nullable-field-convention

Conversation

@1117mg

@1117mg 1117mg commented Aug 8, 2026

Copy link
Copy Markdown
Member

🔗 관련 이슈

📝 작업 내용

  • 응답 DTO: 값이 없을 수 있으면 REQUIRED + nullable = true
  • 요청 DTO: NOT_REQUIRED
  • 빈 목록은 null 이 아니라 []
  • @JsonInclude(NON_NULL) 이나 spring.jackson.default-property-inclusion 사용 X

✅ 체크리스트

  • 로컬에서 빌드 및 테스트가 통과했습니다.
  • 컨벤션(브랜치/커밋 메시지)을 준수했습니다.
  • 관련 문서를 수정했습니다. (필요한 경우)

Summary by CodeRabbit

  • 변경 사항

    • API 응답에서 값이 없는 필드도 키를 유지하며 null로 반환됩니다.
    • nullable 필드가 OpenAPI 문서에서 필수이면서 null 허용으로 명확히 표시됩니다.
    • 응답 래퍼의 data, error, code 필드 규칙이 일관되게 정리되었습니다.
    • 인증, 추천, 시뮬레이션 및 채널 API 예시에 null 반환 사례와 주요 응답 필드가 보강되었습니다.
  • 문서

    • 요청·응답 DTO의 nullable 처리 규칙과 프론트엔드 타입 생성 기준을 문서화했습니다.
  • 테스트

    • nullable 응답과 OpenAPI 스키마 계약 검증을 강화했습니다.

@1117mg 1117mg self-assigned this Aug 8, 2026
@1117mg 1117mg added the refactor label Aug 8, 2026
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

응답 DTO의 null 필드를 키와 함께 반환하도록 직렬화 규칙을 변경했습니다. OpenAPI에서 nullable 필드를 required로 선언하고 $ref 컴포넌트 오염을 방지하는 커스터마이저를 추가했습니다. API 예시와 계약 테스트도 새 규칙을 반영했습니다.

Changes

응답 nullable 계약 표준화

Layer / File(s) Summary
DTO nullable 계약 정의
docs/openapi.md, src/main/java/chaeso/zip/server/*/application/dto/*, src/main/java/chaeso/zip/server/onboarding/presentation/dto/AdHistoryRequest.java
응답의 nullable 필드를 REQUIREDnullable = true로 선언했습니다. 요청 DTO의 선택 필드는 NOT_REQUIRED로 명시했습니다.
OpenAPI nullable 스키마 변환
src/main/java/chaeso/zip/server/common/config/NullableSchemaConfig.java, src/main/java/chaeso/zip/server/common/config/ResponseWrapperSchemaCustomizer.java, src/main/java/chaeso/zip/server/common/response/ApiResponse.java
nullable $ref 프로퍼티를 ComposedSchema로 변환합니다. 공유 컴포넌트의 nullable 상태를 제거합니다. ApiResponsedata, error, code를 필수 nullable 필드로 처리합니다.
응답 직렬화와 API 예시 반영
src/main/java/chaeso/zip/server/auth/presentation/AuthApiDocs.java, src/main/java/chaeso/zip/server/recommendation/..., src/main/java/chaeso/zip/server/simulation/...
NON_NULL 설정을 제거했습니다. Google 인증, 추천, 시뮬레이션 응답 예시에 null 필드를 추가했습니다.
직렬화 및 OpenAPI 계약 검증
src/test/java/chaeso/zip/server/channel/presentation/ChannelControllerTest.java, src/test/java/chaeso/zip/server/recommendation/presentation/RecommendationControllerTest.java, src/test/java/chaeso/zip/server/docs/OpenApiContractTest.java
응답 키가 유지되고 값이 null인지 검증합니다. OpenAPI의 required, nullable, $ref 래핑과 공유 컴포넌트 오염 방지를 검증합니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant APIClient
  participant ResponseDTO
  participant Jackson
  participant OpenAPI
  participant ContractTest
  APIClient->>ResponseDTO: API 응답 요청
  ResponseDTO->>Jackson: nullable 필드 포함 응답 생성
  Jackson-->>APIClient: 키를 유지한 `null` 반환
  ResponseDTO->>OpenAPI: required 및 nullable 스키마 제공
  OpenAPI-->>ContractTest: 래퍼와 DTO 스키마 생성
  ContractTest->>OpenAPI: required, nullable, `$ref` 오염 방지 검증
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 응답 DTO의 선택 필드를 nullable로 통일하는 PR의 주요 변경 사항을 정확하고 간결하게 설명합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/api-nullable-field-convention

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.36%. Comparing base (66438ef) to head (507191f).
⚠️ Report is 2 commits behind head on dev.

Additional details and impacted files
@@             Coverage Diff              @@
##                dev     #147      +/-   ##
============================================
- Coverage     88.56%   88.36%   -0.21%     
- Complexity      506      518      +12     
============================================
  Files           112      112              
  Lines          1583     1607      +24     
  Branches        120      124       +4     
============================================
+ Hits           1402     1420      +18     
- Misses          135      138       +3     
- Partials         46       49       +3     
Files with missing lines Coverage Δ
...chaeso/zip/server/common/response/ApiResponse.java 100.00% <ø> (ø)

... and 4 files with indirect coverage changes

Components Coverage Δ
auth 93.65% <ø> (ø)
channel 51.23% <ø> (ø)
onboarding 87.32% <ø> (ø)
estimation 93.25% <ø> (ø)
performance 91.66% <ø> (ø)
simulation 93.08% <ø> (ø)
user 93.33% <ø> (ø)

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/chaeso/zip/server/channel/application/dto/ProductResponse.java`:
- Around line 18-20: Update ProductResponse.from to normalize
product.getSupportedObjectives() to an empty List.of() when null, and remove
nullable = true from the supportedObjectives `@Schema` declaration so the
documented response type matches the normalized non-null list contract.

In `@src/main/java/chaeso/zip/server/common/config/NullableSchemaConfig.java`:
- Around line 26-29: Update NullableSchemaConfig to represent nullable $ref
properties with anyOf containing the referenced schema and a null ObjectSchema,
removing the ineffective allOf/nullable combination while preserving the
description. Apply the related DTO changes in GoogleAuthResponse and
SimulationItemResponse, and update OpenApiContractTest’s prefill contract
assertions to expect the new anyOf nullable schema.

In `@src/main/java/chaeso/zip/server/common/response/ApiResponse.java`:
- Around line 27-35: Update the OpenAPI examples for ApiResponse.success(...) to
explicitly include error and code as null, and for ApiResponse.fail(...) to
include data and code as null. Also update the 401 response example configured
by CommonResponsesCustomizer, which uses ApiResponse.fail(error), to include
data and code as null while preserving the existing error example.

In
`@src/test/java/chaeso/zip/server/channel/presentation/ChannelControllerTest.java`:
- Around line 225-226: Update the assertion for $.data.products[0].ctr in
ChannelControllerTest to use value(nullValue()) instead of doesNotExist(), so
the test verifies that the nullable key is present with a null value. Keep the
existing assertions for logoUrl and valueMax unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4ef8e62e-eb99-4c8a-8a31-1e1e6e0610ee

📥 Commits

Reviewing files that changed from the base of the PR and between 59b59d7 and 507191f.

📒 Files selected for processing (21)
  • docs/openapi.md
  • src/main/java/chaeso/zip/server/auth/application/dto/GoogleAuthResponse.java
  • src/main/java/chaeso/zip/server/auth/presentation/AuthApiDocs.java
  • src/main/java/chaeso/zip/server/channel/application/dto/AudienceMetricResponse.java
  • src/main/java/chaeso/zip/server/channel/application/dto/ChannelDetailResponse.java
  • src/main/java/chaeso/zip/server/channel/application/dto/ChannelListItemResponse.java
  • src/main/java/chaeso/zip/server/channel/application/dto/PricingResponse.java
  • src/main/java/chaeso/zip/server/channel/application/dto/ProductResponse.java
  • src/main/java/chaeso/zip/server/common/config/NullableSchemaConfig.java
  • src/main/java/chaeso/zip/server/common/config/ResponseWrapperSchemaCustomizer.java
  • src/main/java/chaeso/zip/server/common/response/ApiResponse.java
  • src/main/java/chaeso/zip/server/onboarding/presentation/dto/AdHistoryRequest.java
  • src/main/java/chaeso/zip/server/recommendation/application/dto/RecommendationItemResponse.java
  • src/main/java/chaeso/zip/server/recommendation/presentation/RecommendationApiDocs.java
  • src/main/java/chaeso/zip/server/sample/application/dto/SampleResponse.java
  • src/main/java/chaeso/zip/server/simulation/application/dto/SimulationItemResponse.java
  • src/main/java/chaeso/zip/server/simulation/application/dto/SimulationResponse.java
  • src/main/java/chaeso/zip/server/simulation/presentation/SimulationApiDocs.java
  • src/test/java/chaeso/zip/server/channel/presentation/ChannelControllerTest.java
  • src/test/java/chaeso/zip/server/docs/OpenApiContractTest.java
  • src/test/java/chaeso/zip/server/recommendation/presentation/RecommendationControllerTest.java

Comment on lines 18 to 20
@Schema(description = "지원 광고 목표 코드값 목록", example = "[\"AWARENESS\", \"TRAFFIC\"]",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
List<CampaignObjective> supportedObjectives,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
rg -n -C 4 '\bsupportedObjectives\b|getSupportedObjectives' src/main/java src/test

Repository: YAPP-Github/chaeso-zip-server

Length of output: 7531


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- ProductResponse.java ---'
sed -n '1,80p' src/main/java/chaeso/zip/server/channel/application/dto/ProductResponse.java

printf '%s\n' '--- ChannelProduct.java relevant fields and methods ---'
sed -n '1,120p' src/main/java/chaeso/zip/server/channel/domain/entity/ChannelProduct.java

printf '%s\n' '--- Migrations / SQL for supported_objectives ---'
rg -n -C 3 'supported_objectives|ChannelProduct|channel_products|create.*Product|ALTER TABLE' src/main/resources src/main/java src/test/resources src/test/java || true

printf '%s\n' '--- Tests mentioning response null support / supportedObjectives ---'
rg -n -C 3 'supportedObjectives|ProductResponse|null|지원 광고|광고 목표|requiredMode|nullable' src/test src/main/resources src/main/java/chaeso/zip/server || true

Repository: YAPP-Github/chaeso-zip-server

Length of output: 50386


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- targeted ProductResponse.java ---'
node -e "const fs='fs'; const p='src/main/java/chaeso/zip/server/channel/application/dto/ProductResponse.java'; const s=fs.readFileSync(p,'utf8'); const m=s.match(/@Schema\\(description = \"지원 광고 목표 코드값 목록\"[\\s\\S]*?List<ChallengeObjective> supportedObjectives,|`@Schema`\\(description = \"지원 광고 목표 코드값 목록\"[\\s\\S]*?List<.*CampaignObjective> supportedObjectives,/g);"

rg -n -C 3 '`@Schema`\(description = "지원 광고 목표 코드값 목록"|List<.*CampaignObjective> supportedObjectives|product.getSupportedObjectives\(\)' src/main/java/chaeso/zip/server/channel/application/dto/ProductResponse.java src/main/java/chaeso/zip/server/channel domain entity ChannelProduct.java || true

printf '%s\n' '--- ChannelProduct exact relevant sections ---'
rg -n -C 5 'supportedObjectives|getSupportedObjectives|ChannelProduct\\(' src/main/java/chaeso/zip/server/channel/domain/entity/ChannelProduct.java

printf '%s\n' '--- data/resource references only ---'
fd -a '(\.sql|\.xml|schema|migration|migration\.)' src/main/resources src/test/resources src/main/java src/test/java 2>/dev/null | sed 's#^\./##' | head -200
rg -n -C 3 'supported_objectives|ChannelProduct|channel_products|CREATE TABLE.*Product|ALTER TABLE' src/main/resources src/test/resources src/main/java src/test/java || true

printf '%s\n' '--- focused tests around ProductResponse null behavior ---'
rg -n -C 3 'ProductResponse|supportedObjectives|jsonPath.*products.*supportedObjectives|nullValue|isEmpty\\(\\)' src/test/java src/main/java || true

Repository: YAPP-Github/chaeso-zip-server

Length of output: 1124


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- ProductResponse.java exact lines ---'
sed -n '1,55p' src/main/java/chaeso/zip/server/channel/application/dto/ProductResponse.java

printf '%s\n' '--- ChannelProduct.java entity section ---'
sed -n '1,80p' src/main/java/chaeso/zip/server/channel/domain/entity/ChannelProduct.java

printf '%s\n' '--- supported_objectives references ---'
rg -n -C 4 'supported_objectives|supportedObjectives|supportedObjectives' src/main/resources src/test/resources src/main/java src/test/java || true

printf '%s\n' '--- ProductResponse factory mapping lines ---'
rg -n -C 3 'ProductResponse\\s*\\(|product\\.(getSupportedObjectives|getId|getProductName|getInventoryType|getMinBudgetWon|getMaxBudgetWon|get|getExpected)' src/main/java/chaeso/zip/server/channel/application/dto/ProductResponse.java || true

Repository: YAPP-Github/chaeso-zip-server

Length of output: 10008


빈 목록 규약을 supportedObjectives에도 적용하세요.

supported_objectivesnullable = true로 스웨거 스키마에 노출하면 null 응답을 허용하는 타입이 됩니다. ProductResponse.from에서는 product.getSupportedObjectives()를 그대로 전달하므로, 도메인에서 빈 목록 보장 없으면 null이[]가 아니라 null JSON으로 나옵니다. 응답 작성 시 List.of()로 정규화하고 스키마도 nullable = true를 제거하면 계약과 타입이 일치합니다.

수정 예시
     `@Schema`(description = "지원 광고 목표 코드값 목록", example = "[\"AWARENESS\", \"TRAFFIC\"]",
-        requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
+        requiredMode = Schema.RequiredMode.REQUIRED)
     List<CampaignObjective> supportedObjectives,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/chaeso/zip/server/channel/application/dto/ProductResponse.java`
around lines 18 - 20, Update ProductResponse.from to normalize
product.getSupportedObjectives() to an empty List.of() when null, and remove
nullable = true from the supportedObjectives `@Schema` declaration so the
documented response type matches the normalized non-null list contract.

Comment on lines +26 to +29
return new ComposedSchema()
.addAllOfItem(new Schema<>().$ref(property.get$ref()))
.description(description)
.nullable(true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(NullableSchemaConfig\.java|GoogleAuthResponse\.java|SimulationItemResponse\.java|OpenApiContractTest\.java|pom\.xml|build\.gradle|gradle\.properties)$' || true

echo "== NullableSchemaConfig =="
cat -n src/main/java/chaeso/zip/server/common/config/NullableSchemaConfig.java 2>/dev/null || true

echo "== DTO snippets =="
for f in src/main/java/chaeso/zip/server/auth/application/dto/GoogleAuthResponse.java src/main/java/chaeso/zip/server/simulation/application/dto/SimulationItemResponse.java; do
  echo "--- $f"
  cat -n "$f" 2>/dev/null | sed -n '1,120p'
done

echo "== contract test snippet =="
cat -n src/test/java/chaeso/zip/server/docs/OpenApiContractTest.java 2>/dev/null | sed -n '330,380p'

echo "== relevant dependency versions =="
for f in pom.xml build.gradle gradle.properties; do
  if [ -f "$f" ]; then
    echo "--- $f"
    rg -n "springdoc|openapi|spring-boot|jaxb|swagger" "$f" || true
  fi
done

echo "== schema customizers / annotations around ref nullable =="
rg -n "`@Schema`\\(|springdoc|OASConfig|ApplicationReady|ModelConverter|ModelConverterContext|TypeContext|Customizable|SchemaConverter|OpenAPI|nullable|allOf" src/test/java src/main/java -S || true

Repository: YAPP-Github/chaeso-zip-server

Length of output: 50385


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== inspect generated/openapi files if present =="
git ls-files | rg '(openapi|swagger|api-json|api\.yaml|api\.yml|schema)' || true

echo "== behavioral/parsing probe from source snippets =="
python3 - <<'PY'
import json, textwrap, sys
paths = [
 "src/main/java/chaeso/zip/server/common/config/NullableSchemaConfig.java",
 "src/main/java/chaeso/zip/server/auth/application/dto/GoogleAuthResponse.java",
 "src/main/java/chaeso/zip/server/simulation/application/dto/SimulationItemResponse.java",
 "src/test/java/chaeso/zip/server/docs/OpenApiContractTest.java",
]
for p in paths:
    print(f"--- {p}")
    with open(p) as f:
        lines=f.readlines()
    for i,l in enumerate(lines,1):
        if "ComposedSchema" in l or "new Schema<>().$ref" in l or "nullable(" in l or "assertThat(prefill.path" in l or "requiredMode = Schema.RequiredMode.REQUIRED, nullable = true" in l:
            print(f"{i}: {l.rstrip()}")
PY

Repository: YAPP-Github/chaeso-zip-server

Length of output: 2476


🌐 Web query:

OpenAPI 3.0.4 specification nullable allOf $ref shared schema object type nullable

💡 Result:

In OpenAPI 3.0.4, placing the nullable attribute directly alongside a $ref (at the same level) is invalid and typically ignored by tools [1][2]. This occurs because, per the specification, sibling properties on a $ref are ignored except for summary and description [2]. To make a referenced schema nullable, you should use the allOf keyword to combine the $ref with a local Schema Object that defines the nullability [1][2]. Recommended implementation: allOf: - nullable: true - $ref: '#/components/schemas/YourSchema' Alternatively, some developers use anyOf to achieve similar results, which can be more explicit in how it communicates the 'null or schema' intent [3][4]: anyOf: - nullable: true - $ref: '#/components/schemas/YourSchema' Key points regarding nullable in OpenAPI 3.0: - The nullable attribute is a modifier that, when set to true, adds null to the allowed types of the schema [5][6]. - It is only effective when applied to a Schema Object that also explicitly defines a type (e.g., type: string, type: object) [5][6]. - Because OpenAPI 3.0 schemas follow constraint-based logic, nullable cannot be inherited through $ref or allOf in a way that overrides existing, stricter type constraints in the referenced schema [7][5]. The combination must be valid independently [7]. - OpenAPI 3.0 does not support defining type: [string, null] directly as seen in later JSON Schema versions; the nullable: true modifier is the required mechanism [5][6].

Citations:


🌐 Web query:

OpenAPI 3.0.4 allOf each schema independent validation $ref schema object

💡 Result:

In OpenAPI 3.0.4, the allOf keyword functions as a composition tool based on JSON Schema principles, where each subschema defined within the array is validated independently [1][2][3]. When an instance is validated against a schema containing allOf, it must satisfy every individual subschema provided in the array for the validation to succeed [2][4][5]. Key characteristics of this behavior include: 1. Independent Validation: There is no merging or inheritance of schemas [3][5]. The validator treats each entry in the allOf array as a separate constraint that the data instance must independently pass [1][2]. 2. Schema Composition and $ref: The allOf construct is frequently used to compose models by combining a $ref to a base schema with additional inline properties or constraints [2][6]. For example, you can use allOf to extend a referenced schema by adding new fields, descriptions, or validation requirements [7][4]. 3. No Interaction Among Subschemas: Because each subschema is evaluated independently, there is no inherent interaction between the constraints defined in different subschemas [1][8]. For instance, if one subschema defines a property as a string and another subschema attempts to define the same property as an integer, the resulting schema will be logically invalid because a single value cannot satisfy both constraints simultaneously [3][6]. 4. Not Inheritance: While allOf is often referred to as "inheritance" or "composition," it does not imply a hierarchy [2][5]. If you require polymorphic behavior where an instance should match one of several possible schemas, you should use oneOf or anyOf, typically paired with a discriminator to explicitly define the relationship [9][2][6]. 5. Logical Consistency: Since validation is independent, you must ensure that your combined subschemas do not create contradictory constraints [3][6]. All provided subschemas must be simultaneously satisfiable for the data instance [2].

Citations:


🌐 Web query:

OpenAPI 3.0 specification $ref with siblings allOf each schema validated against allOf member

💡 Result:

In OpenAPI 3.0, the $ref field behaves according to the JSON Reference specification, which dictates that any sibling elements appearing alongside $ref at the same level must be ignored [1][2][3]. Consequently, if you attempt to add properties like description, type, or example as direct siblings to a $ref, those properties will have no effect [1][2]. To work around this limitation in OpenAPI 3.0, the recommended approach is to use allOf [4][5]. By wrapping the $ref within an allOf array, you can effectively combine the referenced schema with additional properties [6][7]. Example usage: yaml components: schemas: MySchema: type: string ExtendedSchema: allOf: - $ref: "`#/components/schemas/MySchema`" description: "An extended description that will be respected." example: "example value" When using this allOf pattern: 1. The referenced schema (e.g., MySchema) is evaluated [7]. 2. The sibling properties defined alongside the allOf (such as description or example) are applied [4][5][7]. 3. This pattern is primarily useful for adding informational properties like description or summary [7]. It is not a mechanism for overriding existing properties within the referenced schema; attempts to override validation-impacting properties (like type) via allOf often lead to undefined or invalid behavior, depending on the tool implementation [8][7]. This behavior is specific to OpenAPI 3.0. OpenAPI 3.1 introduced compatibility with newer JSON Schema drafts, which allow for sibling properties alongside $ref, making the allOf workaround unnecessary for many use cases in later versions [8][5][7].

Citations:


🌐 Web query:

OpenAPI 3.0 specification "any applicable schemas" allOf each Schema Object must validate instance independently

💡 Result:

Yes, in OpenAPI 3.0 (and the underlying JSON Schema specification it relies on), the allOf keyword requires that the instance being validated must successfully validate against each of the schema objects defined within the allOf array independently [1][2][3][4]. Key technical points regarding this behavior include: 1. Independent Validation: Each subschema inside the allOf array is evaluated separately against the instance [1][5][4]. An instance is only considered valid if it satisfies every single subschema provided [2][3][6]. 2. Composition, Not Merging: Although allOf is frequently used to compose models (e.g., combining a base model with specific additions), it does not perform a structural merge of the schemas themselves [7][5]. Because validation is independent, constraints are applied cumulatively [1][5]. This can lead to unexpected results if multiple subschemas define conflicting constraints, such as different types or conflicting additionalProperties settings [1][7][4]. 3. Cumulative Constraints: Because all schemas must be satisfied, the intersection of all constraints determines the final valid state [1]. For example, if one subschema requires property A and another subschema restricts additional properties, the resulting object must satisfy both: it must have property A and it cannot have properties not explicitly defined in the relevant subschemas [1][4]. 4. Specification Source: OpenAPI 3.0 follows JSON Schema validation logic for this behavior [8][9]. The JSON Schema specification explicitly states that an instance validates against allOf if it validates successfully against all schemas defined by the keyword [3]. Because of this independent validation, developers should be cautious when using allOf to override properties, as the resulting schema must still be compatible with all previous subschemas [7][5][4].

Citations:


🌐 Web query:

OpenAPI 3.0 $ref sibling schema properties anyOf type object $ref nullable

💡 Result:

In OpenAPI 3.0, the Reference Object (defined by the $ref keyword) does not allow sibling properties [1][2][3]. According to the specification, any sibling properties accompanying a $ref are ignored by compliant tools [1][4][5]. To include additional properties—such as nullable, examples, or descriptions—alongside a reference, you must use composition keywords like allOf or anyOf [4][6][7]. For example, to correctly define a nullable reference in OpenAPI 3.0, you should wrap the reference in an anyOf or allOf block [8][6][7]: properties: myProperty: anyOf: - $ref: '#/components/schemas/MyObject' - type: 'null' nullable: true In this structure, the nullable keyword applies to the schema object itself, while the $ref is handled within the composition logic [8][7]. This approach avoids the issue of sibling properties being ignored [6][7]. Note that this restriction on sibling properties is a specific characteristic of OpenAPI 3.0 [5][9]. In OpenAPI 3.1, the handling of $ref evolved to align with JSON Schema 2020-12, where sibling keywords are generally allowed on schema references [10][11].

Citations:


nullable $refanyOf로 분기하세요.

prefill, estImpressions, estClicks는 필드 문서에서 null을 허용하지만, 현재 allOf + nullable: true는 OpenAPI 3.0에서 null 허용 범위를 만들지 않습니다. OpenAPI 3.0에서는 $ref 주변 nullable 같은 서브 스키마 속성이 동작하지 않으며, allOf의 각 항목은 독립적으로 검증됩니다.

return new ComposedSchema()
    .addAnyOfItem(new Schema<>().$ref(property.get$ref()))
    .addAnyOfItem(new ObjectSchema().type("null"))
    .description(description);

NullableSchemaConfig, 관련 DTO, 그리고 OpenApiContractTestprefill 계약 검증을 함께 변경하세요.

📍 Affects 4 files
  • src/main/java/chaeso/zip/server/common/config/NullableSchemaConfig.java#L26-L29 (this comment)
  • src/main/java/chaeso/zip/server/auth/application/dto/GoogleAuthResponse.java#L33-L34
  • src/main/java/chaeso/zip/server/simulation/application/dto/SimulationItemResponse.java#L29-L32
  • src/test/java/chaeso/zip/server/docs/OpenApiContractTest.java#L353-L359
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/chaeso/zip/server/common/config/NullableSchemaConfig.java`
around lines 26 - 29, Update NullableSchemaConfig to represent nullable $ref
properties with anyOf containing the referenced schema and a null ObjectSchema,
removing the ineffective allOf/nullable combination while preserving the
description. Apply the related DTO changes in GoogleAuthResponse and
SimulationItemResponse, and update OpenApiContractTest’s prefill contract
assertions to expect the new anyOf nullable schema.

Comment on lines +27 to 35
@Schema(description = "성공 시 응답 본문. 실패 시 null")
private final T data;

@Schema(description = "실패 시 에러 정보. 성공 시 생략")
@Schema(description = "실패 시 에러 정보. 성공 시 null")
private final ErrorResponse error;

@Schema(description = "성공 안내 코드. 안내할 것이 없으면 응답에서 생략")
@Schema(description = "성공 안내 코드. 안내할 것이 없으면 null",
example = "GOOGLE_ACCOUNT_LINKED")
private final String code;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python - <<'PY'
from pathlib import Path
import json
import re
import textwrap

files = [
    "src/main/java/chaeso/zip/server/auth/presentation/AuthApiDocs.java",
    "src/main/java/chaeso/zip/server/recommendation/presentation/RecommendationApiDocs.java",
    "src/main/java/chaeso/zip/server/simulation/presentation/SimulationApiDocs.java",
]

pattern = re.compile(
    r'String\s+([A-Z0-9_]+_EXAMPLE)\s*=\s*"""(.*?)""";',
    re.DOTALL,
)

for filename in files:
    text = Path(filename).read_text()
    for name, body in pattern.findall(text):
        payload = json.loads(textwrap.dedent(body))
        missing = {"data", "error", "code"} - payload.keys()
        if missing:
            print(f"{filename}:{name}: missing {sorted(missing)}")
PY

rg -n -C 2 '"success":|"data":|"error":|"code":' docs/openapi.md

Repository: YAPP-Github/chaeso-zip-server

Length of output: 4565


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== ApiResponse =="
cat -n src/main/java/chaeso/zip/server/common/response/ApiResponse.java | sed -n '1,180p'

echo
echo "== ResponseWrapperSchemaCustomizer =="
rg -n "ResponseWrapperSchemaCustomizer|`@schema`|schema|required|ApiResponse|data|error|code" src/main/java -g '*.java' | sed -n '1,220p'

echo
echo "== OpenAPI wrapper examples =="
sed -n '90,170p' docs/openapi.md

echo
echo "== SecurityConfig auth failure =="
rg -n -C 4 "AuthenticationFailure|AuthenticationEntryPoint|sendFailed|ApiResponse\.fail|fail\\(" src/main/java/chaeso/zip/server/security src/main/java -g '*.java' | sed -n '1,240p'

Repository: YAPP-Github/chaeso-zip-server

Length of output: 44496


ApiResponse 예시와 401 응답을 래퍼 계약에 맞춰 보정하세요.

ApiResponse가 항상 data, error, code를 포함하지만 doc 예시는 종종 생략합니다. ApiResponse.success(...) 예시는 error: null, code: null을, ApiResponse.fail(...) 예시는 data: null, code: null을 추가해 주세요. 이 시점에 401 응답도 CommonResponsesCustomizerApiResponse.fail(error) 예시를 주므로, data: null, code: null을 포함하도록 업데이트하세요.

수정 예시
 {
   "success": true,
   "data": { ... },
+  "error": null,
+  "code": null
 }
 {
   "success": false,
+  "data": null,
   "error": { ... },
+  "code": null
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/chaeso/zip/server/common/response/ApiResponse.java` around
lines 27 - 35, Update the OpenAPI examples for ApiResponse.success(...) to
explicitly include error and code as null, and for ApiResponse.fail(...) to
include data and code as null. Also update the 401 response example configured
by CommonResponsesCustomizer, which uses ApiResponse.fail(error), to include
data and code as null while preserving the existing error example.

Comment on lines +225 to +226
.andExpect(jsonPath("$.data.logoUrl").value(nullValue()))
.andExpect(jsonPath("$.data.products[0].pricing[0].valueMax").value(nullValue()))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

ctr도 null 키 계약으로 검증하세요.

Line 224는 fixture에서 null로 전달한 $.data.products[0].ctr가 없어야 한다고 요구합니다. 이 검증은 nullable 응답 필드를 키와 함께 반환하는 PR 계약과 충돌합니다.

doesNotExist()value(nullValue())로 교체하세요. 그렇지 않으면 올바른 직렬화가 테스트를 실패시키거나, 잘못된 필드 생략이 테스트를 통과합니다.

수정 예시
-        .andExpect(jsonPath("$.data.products[0].ctr").doesNotExist())
+        .andExpect(jsonPath("$.data.products[0].ctr").value(nullValue()))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.andExpect(jsonPath("$.data.logoUrl").value(nullValue()))
.andExpect(jsonPath("$.data.products[0].pricing[0].valueMax").value(nullValue()))
.andExpect(jsonPath("$.data.products[0].ctr").value(nullValue()))
.andExpect(jsonPath("$.data.logoUrl").value(nullValue()))
.andExpect(jsonPath("$.data.products[0].pricing[0].valueMax").value(nullValue()))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/chaeso/zip/server/channel/presentation/ChannelControllerTest.java`
around lines 225 - 226, Update the assertion for $.data.products[0].ctr in
ChannelControllerTest to use value(nullValue()) instead of doesNotExist(), so
the test verifies that the nullable key is present with a null value. Keep the
existing assertions for logoUrl and valueMax unchanged.

@claude

claude Bot commented Aug 8, 2026

Copy link
Copy Markdown

PR 리뷰 요약 (CI 자동 리뷰)

nullable 필드 표기 전환(@JsonInclude(NON_NULL) 제거 + REQUIRED/nullable=true 전환, NullableSchemaConfig/ResponseWrapperSchemaCustomizer 확장)의 구현과 OpenApiContractTest 보강 자체는 의도한 계약과 일치합니다. 다만 이 PR이 바꾼 계약을 검증하지 못하는 기존 테스트가 남아 있어 must-fix로 남깁니다. (해당 테스트 파일들은 이번 PR diff에 포함되어 있지 않아 인라인 코멘트를 달 수 없어 여기에 남깁니다.)

🟠 Major

  • src/test/java/chaeso/zip/server/auth/presentation/AuthControllerTest.java:388-390, 406-407, 425, 466
  • src/test/java/chaeso/zip/server/simulation/presentation/SimulationControllerTest.java:94, 161, 213

이 PR로 GoogleAuthResponse, SimulationResponse, SimulationItemResponse, ApiResponse 등에서 @JsonInclude(NON_NULL)이 제거되어, 값이 없는 필드(linkRequired, signupRequired, accessToken, refreshToken, code, simulationId, items, shortfallWon, error 등)도 이제 키가 null로 항상 실립니다.

그런데 위 테스트들은 여전히 jsonPath(...).doesNotExist()로 검증하고 있습니다. Spring의 JsonPathExpectationsHelper.doesNotExist()는 키가 아예 없을 때뿐 아니라 값이 null일 때도 통과하므로, 이 assert들은 옛 계약(생략)이든 새 계약(null로 실림)이든 항상 그린이라 실제로는 이 PR이 바꾼 동작을 전혀 검증하지 못합니다. 게다가 AuthControllerTest.java:376의 DisplayName("구글 로그인 분기는 토큰만 내려주고 분기 플래그를 싣지 않는다")나 SimulationControllerTest.java:213의 주석("값이 없는 선택 필드는 null 로 담지 않고 생략한다. 스키마의 NOT_REQUIRED 와 같은 계약")은 이 PR이 명시적으로 뒤집은 옛 계약을 그대로 서술하고 있어, 6개월 뒤 이 코드를 보는 사람에게 실제 동작과 반대되는 정보를 줍니다.

같은 PR에서 ChannelControllerTest/RecommendationControllerTest에 적용한 value(nullValue()) 패턴으로 통일하고, DisplayName/주석도 "null로 내려간다"로 갱신해 주세요.

이 외에 병합을 막을 만한 문제는 발견하지 못했습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants