Repository navigation
Description
Summary
schema/compose-spec.json validates a service that declares none of image, build or provider. loader/validate.go rejects that same service.
Minimal reproduction
bad.yml:
services:
app:
container_name: app
restart: alwaysValidated against the official schema:
$ python -m check_jsonschema --schemafile compose-spec.json bad.yml
ok -- validation done
Loaded by compose-go:
$ kompose convert -f bad.yml -o /dev/null # kompose depends on github.com/compose-spec/compose-go/v2 v2.10.0
FATA Unable to load files: service "app" has neither an image nor a build context specified: invalid compose project
Where the two disagree
loader/validate.go, in checkConsistency:
if s.Build == nil && s.Image == "" && s.Provider == nil {
return fmt.Errorf("service %q has neither an image nor a build context specified: %w", s.Name, errdefs.ErrInvalid)
}schema/compose-spec.json defines $defs/service as allOf over two refs:
[0] $ref -> container_spec 72 properties, has `image`, no `required`
[1] $ref -> workload_spec 7 properties, has `build`, no `required`
Neither ref carries required, and the enclosing object adds no anyOf/oneOf. So as far as the schema is concerned, image, build and provider are all optional and all may be absent.
The specification agrees with the loader, in 05-services.md:
imagemay be omitted from a Compose file as long as abuildsection is declared.
Why this matters in practice
This schema is the one editors apply. SchemaStore's catalog maps **/docker-compose.yml, **/compose.yaml and friends to this file, so VS Code, JetBrains and anything else with a JSON-schema language server validate against it while the file is being typed. The result is a compose file that the editor accepts and docker compose up rejects — the error arrives at run time instead of at write time, which is the failure mode the schema exists to prevent.
Suggested fix
Either express the constraint:
"anyOf": [
{"required": ["image"]},
{"required": ["build"]},
{"required": ["provider"]}
]or record it as a reviewed entry in the known-gaps list that loader/tests/completeness_test.go maintains, so that it is a decision rather than an omission. That test currently guarantees every service attribute has a declared test; this is about a relation between attributes, so it is outside what that test can catch.
I have not checked whether provider is expressible in the schema the same way image is, so treat that branch of the suggestion as unverified.
How it came up
I wrote a small mutation tester for validators — it mutates the inputs a checker accepts and reports which mutations get through. Run against JSON schemas, it deleted the image key from a file that validated, and the schema still accepted it. The tool is at https://github.com/simin-yuan/greencheck (MIT, no dependencies) if it is of use, but the reproduction above stands on its own.
Activity
simin-yuan commentedon Sep 29, 2026
Follow-up: I picked up the two branches I had left unverified in the report, and tested both against the schema as vendored in this repository.
provideris expressible the same way as the others. It is a declared property of$defs/service(line 926 ofschema/compose-spec.json), withrequired: ["type"]. So a provider-only service is accepted by the schema today, and thatanyOfbranch closes a real case rather than a hypothetical one. Tested:services: {app: {provider: {type: docker}}}validates against the unmodified schema.$defs/serviceis an object carryingallOfovercontainer_specplusworkload_spec, with a siblingpropertiesblock. AnanyOfnext to thatallOfapplies as intended.Patch tested here against
schema/compose-spec.jsonat blobe4e473bbca37c1fdbd7cfe957644c615b04f284b(main, 2026-09-29), inserted into$defs/serviceas a sibling ofallOf:extendsis not padding. It is resolved beforecheckConsistencyruns, so a service with onlyextendsis legal whenever the base it inherits from supplies one of the other three. Dropping that branch would reject files the loader accepts, which is a worse outcome than the hole being fixed. A service whose base supplies nothing still fails at load time, so this closes the editor-side hole, not every input the loader rejects.Validation with
check-jsonschema0.38.2 (YAML input, so no conversion step):services: {app: {container_name: app, restart: always}}imageonlybuildonlyprovideronlyextendsonlyRegression run: all 42 YAML files under
loader/testdata/at main, validated twice, once with the unmodified schema and once with the patched one. 39 accepted by both, 3 rejected by both (they are fragments, not standalone documents), 0 verdict changes.Second gap in the same pair, same treatment.
loader/validate.golines 242-246:$defs/secretcarries norequiredat all, sosecrets: {s: {}}andsecrets: {s: {name: custom}}validate while the loader rejects both. Tested form:secrets: {s: {}}is ok before and rejected after;file,environmentandexternal: trueall validate both ways. Theconstis deliberate:external: falsefalls through thecontinueand still needs a source.What I did not do, so that nobody reads more into the tables than they say:
schema/schema_test.goand the loader tests were not executed; the regression evidence above is schema against schema over YAML fixtures.build:with no value parses to nil in Go and the loader rejects it, while{"required": ["build"]}accepts it. Closing that would need something like{"properties": {"build": {"not": {"type": "null"}}}, "required": ["build"]}, which I did not test.schema/compose-spec.jsonhere is a copy of the upstream file, so this may need to land incompose-spec/compose-specfirst.